FloatingButton: support call control

Signed-off-by: cjybyjk <cjybyjk@zjnu.edu.cn>
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 54dac14..092ae70 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -9,6 +9,7 @@
     <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS_FULL" />
     <uses-permission android:name="android.permission.READ_PHONE_STATE" />
     <uses-permission android:name="android.permission.MODIFY_PHONE_STATE" />
+    <uses-permission android:name="android.permission.ANSWER_PHONE_CALLS" />
     <uses-permission android:name="android.permission.PROCESS_INCOMING_CALLS" />
 
     <application
diff --git a/app/src/main/java/org/exthmui/game/misc/Constants.java b/app/src/main/java/org/exthmui/game/misc/Constants.java
index 911c67b..5136639 100644
--- a/app/src/main/java/org/exthmui/game/misc/Constants.java
+++ b/app/src/main/java/org/exthmui/game/misc/Constants.java
@@ -38,6 +38,11 @@
         */
         public static final String BROADCAST_GAMING_ACTION = "org.exthmui.game.GAMING_ACTION";
         public static final String BROADCAST_GAMING_MENU_CONTROL = "org.exthmui.game.GAMING_MENU_CONTROL";
+
+        /* 来电状态与控制 */
+        public static final String BROADCAST_CALL_STATUS = "org.exthmui.game.CALL_STATUS";
+        // cmd: 1=挂断 2=接听
+        public static final String BROADCAST_CALL_CONTROL = "org.exthmui.game.CALL_CONTROL";
     }
 
     public static class ConfigKeys {
diff --git a/app/src/main/java/org/exthmui/game/services/GamingService.java b/app/src/main/java/org/exthmui/game/services/GamingService.java
index 936e99f..272c659 100644
--- a/app/src/main/java/org/exthmui/game/services/GamingService.java
+++ b/app/src/main/java/org/exthmui/game/services/GamingService.java
@@ -68,6 +68,8 @@
     private Intent mOverlayServiceIntent;
     private Notification mGamingNotification;
 
+    private Intent mCallStatusIntent;
+
     private String mCurrentPackage;
 
     private Bundle mCurrentConfig = new Bundle();
@@ -89,6 +91,18 @@
         }
     };
 
+    private BroadcastReceiver mCallControlReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            if (mTelecomManager == null) return;
+            if (intent.getIntExtra("cmd", 1) == 1) {
+                mTelecomManager.endCall();
+            } else {
+                mTelecomManager.acceptRingingCall();
+            }
+        }
+    };
+
     private BroadcastReceiver mGamingActionReceiver = new BroadcastReceiver() {
         @Override
         public void onReceive(Context context, Intent intent) {
@@ -137,11 +151,14 @@
         }
 
         registerReceiver(mGamingModeOffReceiver, new IntentFilter(Constants.Broadcasts.SYS_BROADCAST_GAMING_MODE_OFF));
+        LocalBroadcastManager.getInstance(this).registerReceiver(mCallControlReceiver, new IntentFilter(Constants.Broadcasts.BROADCAST_CALL_CONTROL));
         LocalBroadcastManager.getInstance(this).registerReceiver(mGamingActionReceiver, new IntentFilter(Constants.Broadcasts.BROADCAST_GAMING_ACTION));
 
         mPhoneStateListener = new GamingPhoneStateListener();
         mTelephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_CALL_STATE);
 
+        mCallStatusIntent = new Intent(Constants.Broadcasts.BROADCAST_CALL_STATUS);
+
         mOverlayServiceIntent = new Intent(this, OverlayService.class);
 
         PendingIntent stopGamingIntent = PendingIntent.getBroadcast(this, 0, new Intent(Constants.Broadcasts.SYS_BROADCAST_GAMING_MODE_OFF), 0);
@@ -167,6 +184,10 @@
         mOverlayServiceIntent.putExtras(mCurrentConfig);
         startServiceAsUser(mOverlayServiceIntent, UserHandle.CURRENT);
         Settings.System.putInt(getContentResolver(), Settings.System.GAMING_MODE_ACTIVE, 1);
+        if (mTelephonyManager != null) {
+            mCallStatusIntent.putExtra("state", mTelephonyManager.getCallState());
+            LocalBroadcastManager.getInstance(this).sendBroadcast(mCallStatusIntent);
+        }
         return super.onStartCommand(intent, flags, startId);
     }
 
@@ -310,6 +331,7 @@
     @Override
     public void onDestroy() {
         unregisterReceiver(mGamingModeOffReceiver);
+        LocalBroadcastManager.getInstance(this).unregisterReceiver(mCallControlReceiver);
         stopServiceAsUser(mOverlayServiceIntent, UserHandle.CURRENT);
         mTelephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_NONE);
         setDisableGesture(false);
@@ -398,6 +420,8 @@
                         break;
                 }
             }
+            mCallStatusIntent.putExtra("state", state);
+            LocalBroadcastManager.getInstance(GamingService.this).sendBroadcast(mCallStatusIntent);
             mPrevState = state;
             super.onCallStateChanged(state, phoneNumber);
         }
diff --git a/app/src/main/java/org/exthmui/game/services/OverlayService.java b/app/src/main/java/org/exthmui/game/services/OverlayService.java
index 439c33d..ddf92fe 100644
--- a/app/src/main/java/org/exthmui/game/services/OverlayService.java
+++ b/app/src/main/java/org/exthmui/game/services/OverlayService.java
@@ -27,6 +27,7 @@
 import android.os.IBinder;
 import android.preference.PreferenceManager;
 import android.provider.Settings;
+import android.telephony.TelephonyManager;
 import android.text.TextUtils;
 import android.view.Gravity;
 import android.view.LayoutInflater;
@@ -35,6 +36,7 @@
 import android.view.ViewGroup;
 import android.view.WindowManager;
 import android.widget.FrameLayout;
+import android.widget.ImageView;
 import android.widget.LinearLayout;
 import android.widget.ScrollView;
 
@@ -54,7 +56,9 @@
 
     private static final String TAG = "OverlayService";
 
-    private View mGamingFloatingButton;
+    private View mGamingFloatingLayout;
+    private ImageView mGamingFloatingButton;
+    private ImageView mCallControlButton;
     private LinearLayout mGamingOverlayView;
     private ScrollView mGamingMenu;
     private FrameLayout mDanmakuContainer;
@@ -70,6 +74,8 @@
 
     private Bundle configBundle;
 
+    private int mCallStatus = TelephonyManager.CALL_STATE_IDLE;
+
     private OMReceiver mOMReceiver = new OMReceiver();
     private BroadcastReceiver mSysConfigChangedReceiver = new BroadcastReceiver() {
         @Override
@@ -78,6 +84,27 @@
         }
     };
 
+    private BroadcastReceiver mCallStatusReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            if (mCallControlButton == null) return;
+            switch (intent.getIntExtra("state", TelephonyManager.CALL_STATE_IDLE)) {
+                case TelephonyManager.CALL_STATE_RINGING:
+                    mCallControlButton.setImageResource(R.drawable.ic_call_accept);
+                    mCallControlButton.setVisibility(View.VISIBLE);
+                    break;
+                case TelephonyManager.CALL_STATE_IDLE:
+                    mCallControlButton.setVisibility(View.GONE);
+                    break;
+                case TelephonyManager.CALL_STATE_OFFHOOK:
+                    mCallControlButton.setImageResource(R.drawable.ic_call_end);
+                    mCallControlButton.setVisibility(View.VISIBLE);
+                    break;
+            }
+            mCallStatus = intent.getIntExtra("state", TelephonyManager.CALL_STATE_IDLE);
+        }
+    };
+
     private SharedPreferences mPreferences;
 
     public OverlayService() {
@@ -103,6 +130,8 @@
         intentFilter.addAction(Constants.Broadcasts.BROADCAST_GAMING_MENU_CONTROL);
         LocalBroadcastManager.getInstance(this).registerReceiver(mOMReceiver, intentFilter);
 
+        LocalBroadcastManager.getInstance(this).registerReceiver(mCallStatusReceiver, new IntentFilter(Constants.Broadcasts.BROADCAST_CALL_STATUS));
+
         registerReceiver(mSysConfigChangedReceiver, new IntentFilter(Intent.ACTION_CONFIGURATION_CHANGED));
 
         mPreferences = PreferenceManager.getDefaultSharedPreferences(this);
@@ -118,7 +147,7 @@
     private void initView() {
         mWindowManager = (WindowManager) getApplicationContext().getSystemService(Context.WINDOW_SERVICE);
         initGamingMenu();
-        initFloatingButton();
+        initFloatingLayout();
         initDanmaku();
     }
 
@@ -134,7 +163,7 @@
         }
 
         // 悬浮球位置调整
-        if (mGamingFloatingButton != null && mGamingFBLayoutParams != null) {
+        if (mGamingFloatingLayout != null && mGamingFBLayoutParams != null) {
             int defaultX = ((int) getResources().getDimension(R.dimen.game_button_size) - ScreenUtil.getScreenWidth()) / 2;
             if (ScreenUtil.isPortrait()) {
                 mGamingFBLayoutParams.x = mPreferences.getInt(Constants.LocalConfigKeys.FLOATING_BUTTON_COORDINATE_VERTICAL_X, defaultX);
@@ -144,7 +173,7 @@
                 mGamingFBLayoutParams.y = mPreferences.getInt(Constants.LocalConfigKeys.FLOATING_BUTTON_COORDINATE_HORIZONTAL_Y, 10);
             }
             if (mWindowManager != null) {
-                mWindowManager.updateViewLayout(mGamingFloatingButton, mGamingFBLayoutParams);
+                mWindowManager.updateViewLayout(mGamingFloatingLayout, mGamingFBLayoutParams);
             }
         }
 
@@ -190,16 +219,15 @@
         }
     }
 
-    private void initFloatingButton() {
-        if (mGamingFloatingButton == null && mWindowManager != null) {
-            mGamingFloatingButton = LayoutInflater.from(this).inflate(R.layout.gaming_button_layout, null);
+    private void initFloatingLayout() {
+        if (mGamingFloatingLayout == null && mWindowManager != null) {
+            mGamingFloatingLayout = LayoutInflater.from(this).inflate(R.layout.gaming_button_layout, null);
 
             mGamingFBLayoutParams = getBaseLayoutParams();
             mGamingFBLayoutParams.width = WindowManager.LayoutParams.WRAP_CONTENT;
             mGamingFBLayoutParams.height = WindowManager.LayoutParams.WRAP_CONTENT;
 
-            mGamingFloatingButton.setOnClickListener(v -> showHideGamingMenu(0));
-            mGamingFloatingButton.setOnTouchListener(new View.OnTouchListener() {
+            mGamingFloatingLayout.setOnTouchListener(new View.OnTouchListener() {
                 private int origX;
                 private int origY;
                 private int touchX;
@@ -221,7 +249,7 @@
                             mGamingFBLayoutParams.x = origX + x - touchX;
                             mGamingFBLayoutParams.y = origY + y - touchY;
                             if (mWindowManager != null) {
-                                mWindowManager.updateViewLayout(mGamingFloatingButton, mGamingFBLayoutParams);
+                                mWindowManager.updateViewLayout(mGamingFloatingLayout, mGamingFBLayoutParams);
                             }
                             break;
                         case MotionEvent.ACTION_UP:
@@ -248,7 +276,17 @@
                 }
             });
 
-            mWindowManager.addView(mGamingFloatingButton, mGamingFBLayoutParams);
+            mWindowManager.addView(mGamingFloatingLayout, mGamingFBLayoutParams);
+        }
+
+        if (mGamingFloatingButton == null) {
+            mGamingFloatingButton = mGamingFloatingLayout.findViewById(R.id.floating_button);
+            mGamingFloatingButton.setOnClickListener(v -> showHideGamingMenu(0));
+        }
+
+        if (mCallControlButton == null) {
+            mCallControlButton = mGamingFloatingLayout.findViewById(R.id.call_control_button);
+            mCallControlButton.setOnClickListener(v -> callControl());
         }
     }
 
@@ -263,7 +301,7 @@
         if (mGamingOverlayView.getVisibility() == View.VISIBLE && mode != 1) {
             // hide
             mGamingOverlayView.setVisibility(View.GONE);
-            mGamingFloatingButton.setVisibility(View.VISIBLE);
+            mGamingFloatingLayout.setVisibility(View.VISIBLE);
         } else if (mode != 2) {
             // show
             int gravity = 0;
@@ -278,7 +316,7 @@
                 gravity |= Gravity.TOP;
             }
 
-            mGamingFloatingButton.setVisibility(View.GONE);
+            mGamingFloatingLayout.setVisibility(View.GONE);
             mGamingOverlayView.setGravity(gravity);
             ViewGroup.LayoutParams gamingMenuLayoutParams =  mGamingMenu.getLayoutParams();
             if (ScreenUtil.isPortrait()) {
@@ -318,15 +356,22 @@
     @Override
     public void onDestroy() {
         LocalBroadcastManager.getInstance(this).unregisterReceiver(mOMReceiver);
+        LocalBroadcastManager.getInstance(this).unregisterReceiver(mCallStatusReceiver);
         unregisterReceiver(mSysConfigChangedReceiver);
         if (mWindowManager != null) {
-            if (mGamingFloatingButton != null) mWindowManager.removeViewImmediate(mGamingFloatingButton);
+            if (mGamingFloatingLayout != null) mWindowManager.removeViewImmediate(mGamingFloatingLayout);
             if (mDanmakuContainer != null) mWindowManager.removeViewImmediate(mDanmakuContainer);
             if (mGamingOverlayView != null) mWindowManager.removeViewImmediate(mGamingOverlayView);
         }
         super.onDestroy();
     }
 
+    private void callControl() {
+        Intent intent = new Intent(Constants.Broadcasts.BROADCAST_CALL_CONTROL);
+        intent.putExtra("cmd", mCallStatus == TelephonyManager.CALL_STATE_OFFHOOK ? 1 : 2);
+        LocalBroadcastManager.getInstance(this).sendBroadcast(intent);
+    }
+
     private class OMReceiver extends BroadcastReceiver {
         @Override
         public void onReceive(Context context, Intent intent) {
diff --git a/app/src/main/privapp_whitelist_org.exthmui.game.xml b/app/src/main/privapp_whitelist_org.exthmui.game.xml
index 8a06482..596e0f6 100644
--- a/app/src/main/privapp_whitelist_org.exthmui.game.xml
+++ b/app/src/main/privapp_whitelist_org.exthmui.game.xml
@@ -18,5 +18,6 @@
     <privapp-permissions package="org.exthmui.game">
         <permission name="android.permission.MEDIA_CONTENT_CONTROL"/>
         <permission name="android.permission.MODIFY_PHONE_STATE"/>
+        <permission name="android.permission.ANSWER_PHONE_CALLS"/>
     </privapp-permissions>
 </permissions>
diff --git a/app/src/main/res/drawable/ic_call_accept.xml b/app/src/main/res/drawable/ic_call_accept.xml
new file mode 100644
index 0000000..db80ee9
--- /dev/null
+++ b/app/src/main/res/drawable/ic_call_accept.xml
@@ -0,0 +1,17 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+  android:width="108dp"
+  android:height="108dp"
+  android:viewportWidth="108"
+  android:viewportHeight="108">
+  <path
+      android:fillColor="#00AA00"
+      android:pathData="M4,54 A50,50 0 1 0 4,53.99999"/>
+  <group android:scaleX="2.94408"
+      android:scaleY="2.94408"
+      android:translateX="18.67104"
+      android:translateY="18.67104">
+    <path
+        android:fillColor="@android:color/white"
+        android:pathData="M20.01,15.38c-1.23,0 -2.42,-0.2 -3.53,-0.56 -0.35,-0.12 -0.74,-0.03 -1.01,0.24l-1.57,1.97c-2.83,-1.35 -5.48,-3.9 -6.89,-6.83l1.95,-1.66c0.27,-0.28 0.35,-0.67 0.24,-1.02 -0.37,-1.11 -0.56,-2.3 -0.56,-3.53 0,-0.54 -0.45,-0.99 -0.99,-0.99H4.19C3.65,3 3,3.24 3,3.99 3,13.28 10.73,21 20.01,21c0.71,0 0.99,-0.63 0.99,-1.18v-3.45c0,-0.54 -0.45,-0.99 -0.99,-0.99z"/>
+  </group>
+</vector>
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_call_end.xml b/app/src/main/res/drawable/ic_call_end.xml
new file mode 100644
index 0000000..2d878e0
--- /dev/null
+++ b/app/src/main/res/drawable/ic_call_end.xml
@@ -0,0 +1,17 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+  android:width="108dp"
+  android:height="108dp"
+  android:viewportWidth="108"
+  android:viewportHeight="108">
+  <path
+      android:fillColor="#BB0000"
+      android:pathData="M4,54 A50,50 0 1 0 4,53.99999"/>
+  <group android:scaleX="2.94408"
+      android:scaleY="2.94408"
+      android:translateX="18.67104"
+      android:translateY="18.67104">
+    <path
+        android:fillColor="@android:color/white"
+        android:pathData="M12,9c-1.6,0 -3.15,0.25 -4.6,0.72v3.1c0,0.39 -0.23,0.74 -0.56,0.9 -0.98,0.49 -1.87,1.12 -2.66,1.85 -0.18,0.18 -0.43,0.28 -0.7,0.28 -0.28,0 -0.53,-0.11 -0.71,-0.29L0.29,13.08c-0.18,-0.17 -0.29,-0.42 -0.29,-0.7 0,-0.28 0.11,-0.53 0.29,-0.71C3.34,8.78 7.46,7 12,7s8.66,1.78 11.71,4.67c0.18,0.18 0.29,0.43 0.29,0.71 0,0.28 -0.11,0.53 -0.29,0.71l-2.48,2.48c-0.18,0.18 -0.43,0.29 -0.71,0.29 -0.27,0 -0.52,-0.11 -0.7,-0.28 -0.79,-0.74 -1.69,-1.36 -2.67,-1.85 -0.33,-0.16 -0.56,-0.5 -0.56,-0.9v-3.1C15.15,9.25 13.6,9 12,9z"/>
+  </group>
+</vector>
\ No newline at end of file
diff --git a/app/src/main/res/layout/gaming_button_layout.xml b/app/src/main/res/layout/gaming_button_layout.xml
index fc686a3..848585c 100644
--- a/app/src/main/res/layout/gaming_button_layout.xml
+++ b/app/src/main/res/layout/gaming_button_layout.xml
@@ -1,7 +1,10 @@
 <?xml version="1.0" encoding="utf-8"?>
-<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
-    android:layout_width="wrap_content" android:layout_height="wrap_content"
-    android:background="@android:color/transparent">
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:background="@android:color/transparent"
+    android:padding="6dp">
+
     <ImageView
         android:id="@+id/floating_button"
         android:layout_width="@dimen/game_button_size"
@@ -9,4 +12,17 @@
         android:contentDescription="@null"
         android:scaleType="fitCenter"
         android:src="@drawable/ic_game_button" />
-</FrameLayout>
\ No newline at end of file
+
+    <Space
+        android:layout_width="6dp"
+        android:layout_height="wrap_content" />
+
+    <ImageView
+        android:id="@+id/call_control_button"
+        android:layout_width="@dimen/game_button_size"
+        android:layout_height="@dimen/game_button_size"
+        android:contentDescription="@null"
+        android:scaleType="fitCenter"
+        android:src="@drawable/ic_call_end"
+        android:visibility="gone" />
+</LinearLayout>
\ No newline at end of file