From 8b6c64fd2a29a4df9191d72d3a3d455957972843 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Mon, 25 Feb 2019 15:46:55 +0100 Subject: [PATCH] Can respond to incoming calls. --- app/src/main/AndroidManifest.xml | 18 +- .../client/data/helpers/CallsHelper.java | 21 +++ .../client/data/models/CallResponse.java | 34 ++++ .../android/comunic/client/ui/Constants.java | 7 + .../client/ui/activities/BaseActivity.java | 34 +++- .../client/ui/activities/CallActivity.java | 9 +- .../ui/activities/IncomingCallActivity.java | 159 ++++++++++++++++++ .../ui/asynctasks/RespondToCallTask.java | 23 +++ .../PendingCallsBroadcastReceiver.java | 81 ++++++--- .../ui/receivers/RejectCallReceiver.java | 49 ++++++ .../res/layout/activity_incoming_call.xml | 72 ++++++++ app/src/main/res/values-fr/strings.xml | 4 + app/src/main/res/values/colors.xml | 2 +- app/src/main/res/values/strings.xml | 4 + 14 files changed, 482 insertions(+), 35 deletions(-) create mode 100644 app/src/main/java/org/communiquons/android/comunic/client/data/models/CallResponse.java create mode 100644 app/src/main/java/org/communiquons/android/comunic/client/ui/activities/IncomingCallActivity.java create mode 100644 app/src/main/java/org/communiquons/android/comunic/client/ui/asynctasks/RespondToCallTask.java create mode 100644 app/src/main/java/org/communiquons/android/comunic/client/ui/receivers/RejectCallReceiver.java create mode 100644 app/src/main/res/layout/activity_incoming_call.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index e14b038..f35621a 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -74,16 +74,30 @@ + android:label="@string/activity_call_label" /> + + - + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/org/communiquons/android/comunic/client/data/helpers/CallsHelper.java b/app/src/main/java/org/communiquons/android/comunic/client/data/helpers/CallsHelper.java index 851a540..a2de7da 100644 --- a/app/src/main/java/org/communiquons/android/comunic/client/data/helpers/CallsHelper.java +++ b/app/src/main/java/org/communiquons/android/comunic/client/data/helpers/CallsHelper.java @@ -9,6 +9,7 @@ import org.communiquons.android.comunic.client.data.models.APIRequest; 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.CallMember; +import org.communiquons.android.comunic.client.data.models.CallResponse; import org.communiquons.android.comunic.client.data.models.CallsConfiguration; import org.communiquons.android.comunic.client.data.models.NextPendingCallInformation; import org.json.JSONArray; @@ -171,6 +172,26 @@ public class CallsHelper extends BaseHelper { return name; } + /** + * Respond to a call request + * + * @param response The response to the call + * @return TRUE for a success / FALSE else + */ + public boolean respondToCall(@NonNull CallResponse response){ + + APIRequest request = new APIRequest(getContext(), "calls/respond"); + request.addInt("call_id", response.getCallID()); + request.addBoolean("accept", response.isAccept()); + + try { + return request.exec().getJSONObject().has("success"); + } catch (Exception e) { + e.printStackTrace(); + return false; + } + } + /** * Turn a {@link JSONObject} object into a {@link CallsConfiguration} object. diff --git a/app/src/main/java/org/communiquons/android/comunic/client/data/models/CallResponse.java b/app/src/main/java/org/communiquons/android/comunic/client/data/models/CallResponse.java new file mode 100644 index 0000000..1fb6d70 --- /dev/null +++ b/app/src/main/java/org/communiquons/android/comunic/client/data/models/CallResponse.java @@ -0,0 +1,34 @@ +package org.communiquons.android.comunic.client.data.models; + +/** + * Response of a user to a call + * + * @author Pierre HUBERT + */ +public class CallResponse { + + //Private fields + private int callID; + private boolean accept; + + public CallResponse(int callID, boolean accept) { + this.callID = callID; + this.accept = accept; + } + + public int getCallID() { + return callID; + } + + public void setCallID(int callID) { + this.callID = callID; + } + + public boolean isAccept() { + return accept; + } + + public void setAccept(boolean accept) { + this.accept = accept; + } +} diff --git a/app/src/main/java/org/communiquons/android/comunic/client/ui/Constants.java b/app/src/main/java/org/communiquons/android/comunic/client/ui/Constants.java index a8787fc..81df000 100644 --- a/app/src/main/java/org/communiquons/android/comunic/client/ui/Constants.java +++ b/app/src/main/java/org/communiquons/android/comunic/client/ui/Constants.java @@ -72,6 +72,13 @@ public final class Constants { public static final String ACTION_NOTIFY_NEW_CALLS_AVAILABLE = "org.communiquons.android.comunic.client.NEW_CALLS_AVAILABLE"; + + /** + * Intent used to reject incoming call + */ + public static final String ACTION_REJECT_INCOMING_CALL = + "org.communiquons.android.comunic.client.REJECT_INCOMING_CALL"; + } /** diff --git a/app/src/main/java/org/communiquons/android/comunic/client/ui/activities/BaseActivity.java b/app/src/main/java/org/communiquons/android/comunic/client/ui/activities/BaseActivity.java index 1f38cf9..64cb3bd 100644 --- a/app/src/main/java/org/communiquons/android/comunic/client/ui/activities/BaseActivity.java +++ b/app/src/main/java/org/communiquons/android/comunic/client/ui/activities/BaseActivity.java @@ -1,8 +1,8 @@ package org.communiquons.android.comunic.client.ui.activities; import android.os.Bundle; -import android.support.annotation.CallSuper; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.support.v7.app.ActionBar; import android.support.v7.app.AppCompatActivity; @@ -20,6 +20,12 @@ public abstract class BaseActivity extends AppCompatActivity { */ private SafeAsyncTasksManager mSafeAsyncTasksManager = null; + /** + * Current active class + */ + @Nullable + private static String mActiveActivityName = null; + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -36,6 +42,22 @@ public abstract class BaseActivity extends AppCompatActivity { mSafeAsyncTasksManager.unsetAllTasks(); } + @Override + protected void onStart() { + super.onStart(); + + mActiveActivityName = getClass().getSimpleName(); + } + + + @Override + protected void onStop() { + super.onStop(); + + if(getClass().getSimpleName().equals(mActiveActivityName)) + mActiveActivityName = null; + } + @NonNull @Override public ActionBar getSupportActionBar() { @@ -51,4 +73,14 @@ public abstract class BaseActivity extends AppCompatActivity { public SafeAsyncTasksManager getTasksManager() { return mSafeAsyncTasksManager; } + + /** + * Check out whether an activity is the active activity or not + * + * @param activity The activity to check + * @return True if the activity is the active one / false else + */ + public static boolean IsActiveActivity(Class activity){ + return activity.getSimpleName().equals(mActiveActivityName); + } } diff --git a/app/src/main/java/org/communiquons/android/comunic/client/ui/activities/CallActivity.java b/app/src/main/java/org/communiquons/android/comunic/client/ui/activities/CallActivity.java index 5a3b799..df33bdb 100644 --- a/app/src/main/java/org/communiquons/android/comunic/client/ui/activities/CallActivity.java +++ b/app/src/main/java/org/communiquons/android/comunic/client/ui/activities/CallActivity.java @@ -1,7 +1,6 @@ package org.communiquons.android.comunic.client.ui.activities; import android.os.Bundle; -import android.support.v7.app.AppCompatActivity; import android.widget.TextView; import org.communiquons.android.comunic.client.R; @@ -14,7 +13,7 @@ import java.util.Objects; * * @author Pierre HUBERT */ -public class CallActivity extends AppCompatActivity { +public class CallActivity extends BaseActivity { /** * Mandatory argument that includes call id @@ -29,9 +28,6 @@ public class CallActivity extends AppCompatActivity { //Hide call bar Objects.requireNonNull(getSupportActionBar()).hide(); - - ((TextView)findViewById(R.id.call_id)).setText( - "Call " + getIntent().getExtras().getInt(ARGUMENT_CALL_ID)); } @Override @@ -40,5 +36,8 @@ public class CallActivity extends AppCompatActivity { //Hide call notifications PendingCallsBroadcastReceiver.RemoveCallNotification(this); + + ((TextView)findViewById(R.id.call_id)).setText( + "Call " + getIntent().getExtras().getInt(ARGUMENT_CALL_ID)); } } diff --git a/app/src/main/java/org/communiquons/android/comunic/client/ui/activities/IncomingCallActivity.java b/app/src/main/java/org/communiquons/android/comunic/client/ui/activities/IncomingCallActivity.java new file mode 100644 index 0000000..fa0417c --- /dev/null +++ b/app/src/main/java/org/communiquons/android/comunic/client/ui/activities/IncomingCallActivity.java @@ -0,0 +1,159 @@ +package org.communiquons.android.comunic.client.ui.activities; + +import android.content.Intent; +import android.os.AsyncTask; +import android.support.annotation.Nullable; +import android.os.Bundle; +import android.view.View; +import android.widget.Button; +import android.widget.TextView; +import android.widget.Toast; + +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.asynctasks.GetNextPendingCallTask; +import org.communiquons.android.comunic.client.ui.asynctasks.SafeAsyncTask; +import org.communiquons.android.comunic.client.ui.receivers.PendingCallsBroadcastReceiver; +import org.communiquons.android.comunic.client.ui.receivers.RejectCallReceiver; + + +/** + * Incoming call activity + * + * @author Pierre HUBERT + */ +public class IncomingCallActivity extends BaseActivity implements SafeAsyncTask.OnPostExecuteListener, View.OnClickListener { + + private NextPendingCallInformation mNextPendingCallInformation; + + private RefreshThread mRefreshThread = null; + + private Button mAcceptButton; + private Button mRejectButton; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_incoming_call); + + getSupportActionBar().hide(); + + PendingCallsBroadcastReceiver.RemoveCallNotification(this); + + mAcceptButton = findViewById(R.id.accept_button); + mRejectButton = findViewById(R.id.reject_button); + + if(IsActiveActivity(IncomingCallActivity.class)) + finish(); + } + + @Override + protected void onStart() { + super.onStart(); + + mRefreshThread = new RefreshThread(); + mRefreshThread.start(); + + } + + @Override + protected void onStop() { + super.onStop(); + + mRefreshThread.interrupt(); + } + + private void getNextPendingCall() { + //Get next pending call + GetNextPendingCallTask getNextPendingCallTask = new GetNextPendingCallTask(getApplicationContext()); + getNextPendingCallTask.setOnPostExecuteListener(this); + getNextPendingCallTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + getTasksManager().addTask(getNextPendingCallTask); + } + + @Override + public void OnPostExecute(@Nullable NextPendingCallInformation nextPendingCallInformation) { + + if(nextPendingCallInformation == null){ + Toast.makeText(this, R.string.err_get_next_pending_call_info, Toast.LENGTH_SHORT).show(); + return; + } + + if(!nextPendingCallInformation.isHasPendingCall() + || nextPendingCallInformation.hasAllMembersLeftCallExcept(AccountUtils.getID(this))) + finish(); + + this.mNextPendingCallInformation = nextPendingCallInformation; + + ((TextView)findViewById(R.id.call_title)).setText(nextPendingCallInformation.getCallName()); + mAcceptButton.setOnClickListener(this); + mRejectButton.setOnClickListener(this); + } + + @Override + public void onClick(View v) { + + //Accept call + if(v.equals(mAcceptButton)){ + Intent intent = new Intent(this, CallActivity.class); + intent.putExtra(CallActivity.ARGUMENT_CALL_ID, mNextPendingCallInformation.getId()); + startActivity(intent); + finish(); + } + + + //Reject calls + if(v.equals(mRejectButton)){ + Intent intent = new Intent(this, RejectCallReceiver.class); + intent.setAction(Constants.IntentActions.ACTION_REJECT_INCOMING_CALL); + intent.putExtra(RejectCallReceiver.ARGUMENT_CALL_ID, mNextPendingCallInformation.getId()); + sendBroadcast(intent); + finish(); + } + + } + + /** + * Class used to refresh call information + */ + private class RefreshThread extends Thread { + + private boolean stop = false; + private final Object o = new Object(); + + @Override + public void run() { + super.run(); + + while(!stop){ + + //Execute task + runOnUiThread(new Runnable() { + @Override + public void run() { + getNextPendingCall(); + } + }); + + synchronized (o) { + + //Looping + try { + o.wait(2000); + } catch (InterruptedException e) { + e.printStackTrace(); + interrupt(); + } + + } + } + + } + + public void interrupt(){ + stop = true; + } + } +} diff --git a/app/src/main/java/org/communiquons/android/comunic/client/ui/asynctasks/RespondToCallTask.java b/app/src/main/java/org/communiquons/android/comunic/client/ui/asynctasks/RespondToCallTask.java new file mode 100644 index 0000000..def31cc --- /dev/null +++ b/app/src/main/java/org/communiquons/android/comunic/client/ui/asynctasks/RespondToCallTask.java @@ -0,0 +1,23 @@ +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.CallResponse; + +/** + * Respond to call task + * + * @author Pierre HUBERT + */ +public class RespondToCallTask extends SafeAsyncTask { + + public RespondToCallTask(Context context) { + super(context); + } + + @Override + protected Boolean doInBackground(CallResponse... callResponses) { + return new CallsHelper(getContext()).respondToCall(callResponses[0]); + } +} diff --git a/app/src/main/java/org/communiquons/android/comunic/client/ui/receivers/PendingCallsBroadcastReceiver.java b/app/src/main/java/org/communiquons/android/comunic/client/ui/receivers/PendingCallsBroadcastReceiver.java index 8c847da..d6304b5 100644 --- a/app/src/main/java/org/communiquons/android/comunic/client/ui/receivers/PendingCallsBroadcastReceiver.java +++ b/app/src/main/java/org/communiquons/android/comunic/client/ui/receivers/PendingCallsBroadcastReceiver.java @@ -18,7 +18,9 @@ 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.BaseActivity; import org.communiquons.android.comunic.client.ui.activities.CallActivity; +import org.communiquons.android.comunic.client.ui.activities.IncomingCallActivity; 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; @@ -50,6 +52,11 @@ public class PendingCallsBroadcastReceiver extends BroadcastReceiver { */ private boolean locked = false; + /** + * The ID of the last shown notification + */ + private static int mLastNotificationCallID = 0; + @Override public void onReceive(final Context context, Intent intent) { @@ -61,6 +68,12 @@ public class PendingCallsBroadcastReceiver extends BroadcastReceiver { throw new RuntimeException("Unexpected call of " + TAG); + //Check if user is already calling + if(CheckIfUseless(context, null)) { + Log.v(TAG, "Reported that a new call is available but skipped because considered as useless."); + return; + } + //Check if service is currently locked if(locked) { Log.e(TAG, "New call skipped because this class is locked"); @@ -93,29 +106,20 @@ public class PendingCallsBroadcastReceiver extends BroadcastReceiver { return; } - //Check if there is no pending call - if(!info.isHasPendingCall()) { - - //Remove any related notification - RemoveCallNotification(context); + //Check if it is useless to continue + if(CheckIfUseless(context, info)) { 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); + //Full screen call request intent + Intent fullScreenCallRequestIntent = new Intent(context, IncomingCallActivity.class); + PendingIntent pendingFullScreenRequestIntent = PendingIntent.getActivity(context, + 0, fullScreenCallRequestIntent, 0); + //Create and show notification NotificationCompat.Builder builder = @@ -123,14 +127,9 @@ public class PendingCallsBroadcastReceiver extends BroadcastReceiver { .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) + //.setContentIntent(pendingAcceptIntent) + .setFullScreenIntent(pendingFullScreenRequestIntent, true) + .setPriority(PRIORITY_HIGH); //Create notification channel if required @@ -146,7 +145,28 @@ public class PendingCallsBroadcastReceiver extends BroadcastReceiver { Notification n = builder.build(); n.flags |= Notification.FLAG_ONGOING_EVENT | Notification.FLAG_NO_CLEAR; - NotificationManagerCompat.from(context).notify(CALL_NOTIFICATION_ID, n); + mLastNotificationCallID = info.getId(); + NotificationManagerCompat.from(context).notify(GetNotificationID(mLastNotificationCallID), n); + } + + /** + * Check if calling this receiver is useless for now + * + * @param context Context of application + * @param call Optional information about a pending call + * @return TRUE if useless / FALSE else + */ + public static boolean CheckIfUseless(Context context, @Nullable NextPendingCallInformation call){ + boolean useless = BaseActivity.IsActiveActivity(IncomingCallActivity.class) + || BaseActivity.IsActiveActivity(CallActivity.class) + || (call != null && ( + !call.isHasPendingCall() + || call.hasAllMembersLeftCallExcept(AccountUtils.getID(context)))); + + if(useless) + RemoveCallNotification(context); + + return useless; } /** @@ -155,6 +175,15 @@ public class PendingCallsBroadcastReceiver extends BroadcastReceiver { * @param context The context of the application */ public static void RemoveCallNotification(Context context){ - NotificationManagerCompat.from(context).cancel(CALL_NOTIFICATION_ID); + NotificationManagerCompat.from(context).cancel(GetNotificationID(mLastNotificationCallID)); + } + + /** + * Get the ID of a notification for a call + * + * @param callID The ID of the target call + */ + private static int GetNotificationID(int callID){ + return CALL_NOTIFICATION_ID*1000 + callID; } } diff --git a/app/src/main/java/org/communiquons/android/comunic/client/ui/receivers/RejectCallReceiver.java b/app/src/main/java/org/communiquons/android/comunic/client/ui/receivers/RejectCallReceiver.java new file mode 100644 index 0000000..2e760b7 --- /dev/null +++ b/app/src/main/java/org/communiquons/android/comunic/client/ui/receivers/RejectCallReceiver.java @@ -0,0 +1,49 @@ +package org.communiquons.android.comunic.client.ui.receivers; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.os.AsyncTask; +import android.util.Log; + +import org.communiquons.android.comunic.client.data.models.CallResponse; +import org.communiquons.android.comunic.client.ui.asynctasks.RespondToCallTask; + +import java.util.Objects; + +import static org.communiquons.android.comunic.client.ui.Constants.IntentActions.ACTION_REJECT_INCOMING_CALL; + +/** + * This broadcast receiver is used to reject incoming calls + * + * @author Pierre HUBERT + */ +public class RejectCallReceiver extends BroadcastReceiver { + + /** + * Debug tag + */ + private static final String TAG = RejectCallReceiver.class.getSimpleName(); + + /** + * Mandatory argument that includes call id + */ + public static final String ARGUMENT_CALL_ID = "call_id"; + + @Override + public void onReceive(Context context, Intent intent) { + + if(intent == null || !ACTION_REJECT_INCOMING_CALL.equals(intent.getAction())) + throw new RuntimeException(TAG + " was incorrectly triggered!"); + + int callID = Objects.requireNonNull(intent.getExtras()).getInt(ARGUMENT_CALL_ID); + + //Send the response back to the server + Log.v(TAG, "Reject call " + callID); + RespondToCallTask respondToCallTask = new RespondToCallTask(context); + respondToCallTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, new CallResponse( + callID, + false + )); + } +} diff --git a/app/src/main/res/layout/activity_incoming_call.xml b/app/src/main/res/layout/activity_incoming_call.xml new file mode 100644 index 0000000..2b8c061 --- /dev/null +++ b/app/src/main/res/layout/activity_incoming_call.xml @@ -0,0 +1,72 @@ + + + + + + + +