diff --git a/contacts/build.properties b/contacts/build.properties
index 50bc646..7638e8d 100644
--- a/contacts/build.properties
+++ b/contacts/build.properties
@@ -1,8 +1,8 @@
#Created by .winboll/winboll_app_build.gradle
-#Thu Feb 20 09:27:14 GMT 2025
+#Thu Feb 20 22:09:07 GMT 2025
stageCount=0
libraryProject=winboll-shared
baseVersion=1.0
publishVersion=1.0.0
-buildCount=99
+buildCount=126
baseBetaVersion=1.0.1
diff --git a/contacts/src/main/AndroidManifest.xml b/contacts/src/main/AndroidManifest.xml
index fbba644..bf357cb 100644
--- a/contacts/src/main/AndroidManifest.xml
+++ b/contacts/src/main/AndroidManifest.xml
@@ -9,6 +9,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
-
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -91,8 +165,20 @@
-
+
+
+
+
+
+
+
-
\ No newline at end of file
+
diff --git a/contacts/src/main/java/cc/winboll/studio/contacts/ActivityStack.java b/contacts/src/main/java/cc/winboll/studio/contacts/ActivityStack.java
new file mode 100644
index 0000000..2f37ec0
--- /dev/null
+++ b/contacts/src/main/java/cc/winboll/studio/contacts/ActivityStack.java
@@ -0,0 +1,59 @@
+package cc.winboll.studio.contacts;
+
+import android.app.Activity;
+
+import java.util.ArrayList;
+import java.util.List;
+
+
+public class ActivityStack {
+
+ private static final ActivityStack INSTANCE = new ActivityStack();
+
+ private List activities = new ArrayList<>();
+
+ public static ActivityStack getInstance() {
+ return INSTANCE;
+ }
+
+ public void addActivity(Activity activity) {
+ activities.add(activity);
+ }
+
+ public Activity getTopActivity() {
+ if (activities.isEmpty()) {
+ return null;
+ }
+ return activities.get(activities.size() - 1);
+ }
+
+ public void finishTopActivity() {
+ if (!activities.isEmpty()) {
+ activities.remove(activities.size() - 1).finish();
+ }
+ }
+
+ public void finishActivity(Activity activity) {
+ if (activity != null) {
+ activities.remove(activity);
+ activity.finish();
+ }
+ }
+
+ public void finishActivity(Class activityClass) {
+ for (Activity activity : activities) {
+ if (activity.getClass().equals(activityClass)) {
+ finishActivity(activity);
+ }
+ }
+ }
+
+ public void finishAllActivity() {
+ if (!activities.isEmpty()) {
+ for (Activity activity : activities) {
+ activity.finish();
+ activities.remove(activity);
+ }
+ }
+ }
+}
diff --git a/contacts/src/main/java/cc/winboll/studio/contacts/MainActivity.java b/contacts/src/main/java/cc/winboll/studio/contacts/MainActivity.java
index 5ccbc69..24f9f4f 100644
--- a/contacts/src/main/java/cc/winboll/studio/contacts/MainActivity.java
+++ b/contacts/src/main/java/cc/winboll/studio/contacts/MainActivity.java
@@ -1,30 +1,52 @@
package cc.winboll.studio.contacts;
+import android.Manifest;
+import android.annotation.SuppressLint;
+import android.app.Activity;
+import android.app.ActivityManager;
+import android.app.role.RoleManager;
+import android.content.Context;
import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.net.Uri;
+import android.os.Build;
import android.os.Bundle;
+import android.provider.Settings;
+import android.telecom.TelecomManager;
+import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
+import android.view.View;
+import android.view.WindowManager;
import android.widget.CheckBox;
-import com.google.android.material.tabs.TabLayout;
+import android.widget.CompoundButton;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.Switch;
+import android.widget.Toast;
+import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
+import androidx.core.content.ContextCompat;
import androidx.viewpager.widget.ViewPager;
import cc.winboll.studio.contacts.R;
+import cc.winboll.studio.contacts.activities.CallActivity;
import cc.winboll.studio.contacts.adapters.MyPagerAdapter;
import cc.winboll.studio.contacts.beans.MainServiceBean;
+import cc.winboll.studio.contacts.services.MainService;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.LogView;
import cc.winboll.studio.libapputils.app.IWinBollActivity;
import cc.winboll.studio.libapputils.app.WinBollActivityManager;
import cc.winboll.studio.libapputils.bean.APPInfo;
import cc.winboll.studio.libapputils.view.YesNoAlertDialog;
-import android.widget.ImageView;
-import android.view.View;
+import cc.winboll.studio.contacts.listenphonecall.CallListenerService;
+import com.google.android.material.tabs.TabLayout;
+import java.lang.reflect.Field;
import java.util.ArrayList;
-import android.view.LayoutInflater;
-import android.widget.LinearLayout;
import java.util.List;
-import cc.winboll.studio.contacts.activities.CallActivity;
+import android.content.DialogInterface;
+import cc.winboll.studio.contacts.activities.SettingsActivity;
final public class MainActivity extends AppCompatActivity implements IWinBollActivity, ViewPager.OnPageChangeListener, View.OnClickListener {
@@ -48,6 +70,8 @@ final public class MainActivity extends AppCompatActivity implements IWinBollAct
LinearLayout linearLayout;//下标所在在LinearLayout布局里
int currentPoint = 0;//当前被选中中页面的下标
+ private static final int DIALER_REQUEST_CODE = 1;
+
@Override
public AppCompatActivity getActivity() {
return this;
@@ -90,7 +114,7 @@ final public class MainActivity extends AppCompatActivity implements IWinBollAct
getSupportActionBar().setSubtitle(getTag());
initData();
- initView();//调用初始化视图方法
+ initView();
//initPoint();//调用初始化导航原点的方法
viewPager.addOnPageChangeListener(this);//滑动事件
@@ -116,9 +140,9 @@ final public class MainActivity extends AppCompatActivity implements IWinBollAct
// }
// }
// });
+ MainService.startMainService(MainActivity.this);
}
-
//初始化view,即显示的图片
void initView() {
viewPager = findViewById(R.id.activitymainViewPager1);
@@ -311,6 +335,10 @@ final public class MainActivity extends AppCompatActivity implements IWinBollAct
Intent intent = new Intent(this, CallActivity.class);
startActivity(intent);
//WinBollActivityManager.getInstance(this).startWinBollActivity(this, CallActivity.class);
+ } else if (item.getItemId() == R.id.item_settings) {
+ Intent intent = new Intent(this, SettingsActivity.class);
+ startActivity(intent);
+ //WinBollActivityManager.getInstance(this).startWinBollActivity(this, CallActivity.class);
}
// } else
// if (item.getItemId() == R.id.item_exit) {
@@ -320,20 +348,59 @@ final public class MainActivity extends AppCompatActivity implements IWinBollAct
return super.onOptionsItemSelected(item);
}
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ }
+
+ /**
+ * Android M 及以上检查是否是系统默认电话应用
+ */
+ public boolean isDefaultPhoneCallApp() {
+ if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ TelecomManager manger = (TelecomManager) getSystemService(TELECOM_SERVICE);
+ if (manger != null && manger.getDefaultDialerPackage() != null) {
+ return manger.getDefaultDialerPackage().equals(getPackageName());
+ }
+ }
+ return false;
+ }
+
+ public boolean isServiceRunning(Class> serviceClass) {
+ ActivityManager manager = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);
+ if (manager == null) return false;
+
+ for (ActivityManager.RunningServiceInfo service : manager.getRunningServices(
+ Integer.MAX_VALUE)) {
+ if (serviceClass.getName().equals(service.service.getClassName())) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
- switch (resultCode) {
- case REQUEST_HOME_ACTIVITY : {
- LogUtils.d(TAG, "REQUEST_HOME_ACTIVITY");
- break;
- }
- case REQUEST_ABOUT_ACTIVITY : {
- LogUtils.d(TAG, "REQUEST_ABOUT_ACTIVITY");
- break;
- }
- default : {
- super.onActivityResult(requestCode, resultCode, data);
- }
+// switch (resultCode) {
+// case REQUEST_HOME_ACTIVITY : {
+// LogUtils.d(TAG, "REQUEST_HOME_ACTIVITY");
+// break;
+// }
+// case REQUEST_ABOUT_ACTIVITY : {
+// LogUtils.d(TAG, "REQUEST_ABOUT_ACTIVITY");
+// break;
+// }
+// default : {
+// super.onActivityResult(requestCode, resultCode, data);
+// }
+// }
+ if (requestCode == DIALER_REQUEST_CODE) {
+ if (resultCode == Activity.RESULT_OK) {
+ Toast.makeText(MainActivity.this, getString(R.string.app_name) + " 已成为默认电话应用",
+ Toast.LENGTH_SHORT).show();
+ }
}
}
}
diff --git a/contacts/src/main/java/cc/winboll/studio/contacts/PhoneCallManager.java b/contacts/src/main/java/cc/winboll/studio/contacts/PhoneCallManager.java
new file mode 100644
index 0000000..7f7292f
--- /dev/null
+++ b/contacts/src/main/java/cc/winboll/studio/contacts/PhoneCallManager.java
@@ -0,0 +1,35 @@
+package cc.winboll.studio.contacts;
+
+/**
+ * @Author ZhanGSKen@AliYun.Com
+ * @Date 2025/02/20 21:14:52
+ * @Describe PhoneCallManager
+ */
+
+import android.telecom.Call;
+import android.telecom.VideoProfile;
+
+public class PhoneCallManager {
+
+ public static final String TAG = "PhoneCallManager";
+
+ public static Call call;
+
+ /**
+ * 接听电话
+ */
+ public void answer() {
+ if (call != null) {
+ call.answer(VideoProfile.STATE_AUDIO_ONLY);
+ }
+ }
+
+ /**
+ * 断开电话,包括来电时的拒接以及接听后的挂断
+ */
+ public void disconnect() {
+ if (call != null) {
+ call.disconnect();
+ }
+ }
+}
diff --git a/contacts/src/main/java/cc/winboll/studio/contacts/activities/DialerActivity.java b/contacts/src/main/java/cc/winboll/studio/contacts/activities/DialerActivity.java
new file mode 100644
index 0000000..ed1afa8
--- /dev/null
+++ b/contacts/src/main/java/cc/winboll/studio/contacts/activities/DialerActivity.java
@@ -0,0 +1,40 @@
+package cc.winboll.studio.contacts.activities;
+
+/**
+ * @Author ZhanGSKen@AliYun.Com
+ * @Date 2025/02/20 20:18:26
+ */
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.view.View;
+import android.widget.Button;
+import android.widget.EditText;
+import androidx.appcompat.app.AppCompatActivity;
+import cc.winboll.studio.contacts.R;
+
+public class DialerActivity extends AppCompatActivity {
+
+ public static final String TAG = "DialerActivity";
+
+ private EditText phoneNumberEditText;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_dialer);
+
+ phoneNumberEditText = findViewById(R.id.phone_number_edit_text);
+ Button dialButton = findViewById(R.id.dial_button);
+
+ dialButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ String phoneNumber = phoneNumberEditText.getText().toString();
+ Intent intent = new Intent(Intent.ACTION_DIAL, Uri.parse("tel:" + phoneNumber));
+ startActivity(intent);
+ }
+ });
+ }
+}
+
diff --git a/contacts/src/main/java/cc/winboll/studio/contacts/activities/SettingsActivity.java b/contacts/src/main/java/cc/winboll/studio/contacts/activities/SettingsActivity.java
new file mode 100644
index 0000000..ef2e3fa
--- /dev/null
+++ b/contacts/src/main/java/cc/winboll/studio/contacts/activities/SettingsActivity.java
@@ -0,0 +1,138 @@
+package cc.winboll.studio.contacts.activities;
+
+/**
+ * @Author ZhanGSKen@AliYun.Com
+ * @Date 2025/02/21 05:37:42
+ */
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.provider.Settings;
+import android.view.View;
+import android.view.WindowManager;
+import android.widget.Toast;
+import androidx.appcompat.app.AlertDialog;
+import androidx.appcompat.app.AppCompatActivity;
+import cc.winboll.studio.contacts.R;
+import com.hjq.toast.ToastUtils;
+import java.lang.reflect.Field;
+import androidx.appcompat.widget.Toolbar;
+import cc.winboll.studio.libappbase.IWinBollActivity;
+import cc.winboll.studio.libappbase.bean.APPInfo;
+
+public class SettingsActivity extends AppCompatActivity implements IWinBollActivity {
+
+ public static final String TAG = "SettingsActivity";
+
+ Toolbar mToolbar;
+
+ @Override
+ public APPInfo getAppInfo() {
+ return null;
+ }
+
+ @Override
+ public AppCompatActivity getActivity() {
+ return this;
+ }
+
+ @Override
+ public String getTag() {
+ return TAG;
+ }
+
+ @Override
+ public Toolbar initToolBar() {
+ return findViewById(R.id.activitymainToolbar1);
+ }
+
+ @Override
+ public boolean isAddWinBollToolBar() {
+ return true;
+ }
+
+ @Override
+ public boolean isEnableDisplayHomeAsUp() {
+ return false;
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_settings);
+
+ // 初始化工具栏
+ mToolbar = findViewById(R.id.activitymainToolbar1);
+ setSupportActionBar(mToolbar);
+ if (isEnableDisplayHomeAsUp()) {
+ // 显示后退按钮
+ getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+ }
+ getSupportActionBar().setSubtitle(getTag());
+
+ }
+
+ public void onDefaultPhone(View view) {
+ Intent intent = new Intent(Settings.ACTION_MANAGE_DEFAULT_APPS_SETTINGS);
+ startActivity(intent);
+ }
+ public void onCanDrawOverlays(View view) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
+ && !Settings.canDrawOverlays(this)) {
+ // 请求 悬浮框 权限
+ askForDrawOverlay();
+ } else {
+ ToastUtils.show("悬浮窗已开启");
+ }
+ }
+
+ private void askForDrawOverlay() {
+ AlertDialog alertDialog = new AlertDialog.Builder(this)
+ .setTitle("允许显示悬浮框")
+ .setMessage("为了使电话监听服务正常工作,请允许这项权限")
+ .setPositiveButton("去设置", new DialogInterface.OnClickListener(){
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ openDrawOverlaySettings();
+ dialog.dismiss();
+ }
+ })
+ .setNegativeButton("稍后再说", new DialogInterface.OnClickListener(){
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ dialog.dismiss();
+ }
+ })
+ .create();
+
+ //noinspection ConstantConditions
+ alertDialog.getWindow().setFlags(
+ WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
+ WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE);
+ alertDialog.show();
+ }
+
+ /**
+ * 跳转悬浮窗管理设置界面
+ */
+ private void openDrawOverlaySettings() {
+ if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ // Android M 以上引导用户去系统设置中打开允许悬浮窗
+ // 使用反射是为了用尽可能少的代码保证在大部分机型上都可用
+ try {
+ Context context = this;
+ Class clazz = Settings.class;
+ Field field = clazz.getDeclaredField("ACTION_MANAGE_OVERLAY_PERMISSION");
+ Intent intent = new Intent(field.get(null).toString());
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ intent.setData(Uri.parse("package:" + context.getPackageName()));
+ context.startActivity(intent);
+ } catch (Exception e) {
+ Toast.makeText(this, "请在悬浮窗管理中打开权限", Toast.LENGTH_LONG).show();
+ }
+ }
+ }
+}
diff --git a/contacts/src/main/java/cc/winboll/studio/contacts/listenphonecall/CallListenerService.java b/contacts/src/main/java/cc/winboll/studio/contacts/listenphonecall/CallListenerService.java
new file mode 100644
index 0000000..f555114
--- /dev/null
+++ b/contacts/src/main/java/cc/winboll/studio/contacts/listenphonecall/CallListenerService.java
@@ -0,0 +1,206 @@
+package cc.winboll.studio.contacts.listenphonecall;
+
+import android.app.Service;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.graphics.PixelFormat;
+import android.os.Build;
+import android.os.IBinder;
+import android.telephony.PhoneStateListener;
+import android.telephony.TelephonyManager;
+import android.text.TextUtils;
+import android.view.Gravity;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.WindowManager;
+import android.widget.Button;
+import android.widget.FrameLayout;
+import android.widget.TextView;
+import androidx.annotation.Nullable;
+import cc.winboll.studio.contacts.MainActivity;
+import cc.winboll.studio.contacts.R;
+
+
+public class CallListenerService extends Service {
+
+ private View phoneCallView;
+ private TextView tvCallNumber;
+ private Button btnOpenApp;
+
+ private WindowManager windowManager;
+ private WindowManager.LayoutParams params;
+
+ private PhoneStateListener phoneStateListener;
+ private TelephonyManager telephonyManager;
+
+ private String callNumber;
+ private boolean hasShown;
+ private boolean isCallingIn;
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+
+ initPhoneStateListener();
+
+ initPhoneCallView();
+ }
+
+ @Nullable
+ @Override
+ public IBinder onBind(Intent intent) {
+ return null;
+ }
+
+ /**
+ * 初始化来电状态监听器
+ */
+ private void initPhoneStateListener() {
+ phoneStateListener = new PhoneStateListener() {
+ @Override
+ public void onCallStateChanged(int state, String incomingNumber) {
+ super.onCallStateChanged(state, incomingNumber);
+
+ callNumber = incomingNumber;
+
+ switch (state) {
+ case TelephonyManager.CALL_STATE_IDLE: // 待机,即无电话时,挂断时触发
+ dismiss();
+ break;
+
+ case TelephonyManager.CALL_STATE_RINGING: // 响铃,来电时触发
+ isCallingIn = true;
+ updateUI();
+ show();
+ break;
+
+ case TelephonyManager.CALL_STATE_OFFHOOK: // 摘机,接听或拨出电话时触发
+ updateUI();
+ show();
+ break;
+
+ default:
+ break;
+
+ }
+ }
+ };
+
+ // 设置来电监听器
+ telephonyManager = (TelephonyManager) getSystemService(TELEPHONY_SERVICE);
+ if (telephonyManager != null) {
+ telephonyManager.listen(phoneStateListener, PhoneStateListener.LISTEN_CALL_STATE);
+ }
+
+ }
+
+ private void initPhoneCallView() {
+ windowManager = (WindowManager) getApplicationContext()
+ .getSystemService(Context.WINDOW_SERVICE);
+ int width = windowManager.getDefaultDisplay().getWidth();
+ int height = windowManager.getDefaultDisplay().getHeight();
+
+ params = new WindowManager.LayoutParams();
+ params.gravity = Gravity.CENTER_HORIZONTAL | Gravity.TOP;
+ params.width = width;
+ params.height = WindowManager.LayoutParams.WRAP_CONTENT;
+ params.screenOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
+
+ // 设置图片格式,效果为背景透明
+ params.format = PixelFormat.TRANSLUCENT;
+ // 设置 Window flag 为系统级弹框 | 覆盖表层
+ params.type = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O ?
+ WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY :
+ WindowManager.LayoutParams.TYPE_PHONE;
+
+ // 不可聚集(不响应返回键)| 全屏
+ params.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
+ | WindowManager.LayoutParams.FLAG_FULLSCREEN
+ | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN;
+ // API 19 以上则还可以开启透明状态栏与导航栏
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
+ params.flags = WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS
+ | WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION
+ | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
+ | WindowManager.LayoutParams.FLAG_FULLSCREEN
+ | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN;
+ }
+
+ FrameLayout interceptorLayout = new FrameLayout(this) {
+
+ @Override
+ public boolean dispatchKeyEvent(KeyEvent event) {
+
+ if (event.getAction() == KeyEvent.ACTION_DOWN) {
+ if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
+
+ return true;
+ }
+ }
+
+ return super.dispatchKeyEvent(event);
+ }
+ };
+
+ phoneCallView = ((LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE))
+ .inflate(R.layout.view_phone_call, interceptorLayout);
+ tvCallNumber = phoneCallView.findViewById(R.id.tv_call_number);
+ btnOpenApp = phoneCallView.findViewById(R.id.btn_open_app);
+ btnOpenApp.setOnClickListener(new View.OnClickListener(){
+
+ @Override
+ public void onClick(View view) {
+ Intent intent = new Intent(getApplicationContext(), MainActivity.class);
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ CallListenerService.this.startActivity(intent);
+ }
+ });
+ }
+
+ /**
+ * 显示顶级弹框展示通话信息
+ */
+ private void show() {
+ if (!hasShown) {
+ windowManager.addView(phoneCallView, params);
+ hasShown = true;
+ }
+ }
+
+ /**
+ * 取消显示
+ */
+ private void dismiss() {
+ if (hasShown) {
+ windowManager.removeView(phoneCallView);
+ isCallingIn = false;
+ hasShown = false;
+ }
+ }
+
+ private void updateUI() {
+ tvCallNumber.setText(formatPhoneNumber(callNumber));
+
+ int callTypeDrawable = isCallingIn ? R.drawable.ic_phone_call_in : R.drawable.ic_phone_call_out;
+ tvCallNumber.setCompoundDrawablesWithIntrinsicBounds(null, null,
+ getResources().getDrawable(callTypeDrawable), null);
+ }
+
+ public static String formatPhoneNumber(String phoneNum) {
+ if (!TextUtils.isEmpty(phoneNum) && phoneNum.length() == 11) {
+ return phoneNum.substring(0, 3) + "-"
+ + phoneNum.substring(3, 7) + "-"
+ + phoneNum.substring(7);
+ }
+ return phoneNum;
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+
+ telephonyManager.listen(phoneStateListener, PhoneStateListener.LISTEN_NONE);
+ }
+}
diff --git a/contacts/src/main/java/cc/winboll/studio/contacts/phonecallui/PhoneCallActivity.java b/contacts/src/main/java/cc/winboll/studio/contacts/phonecallui/PhoneCallActivity.java
new file mode 100644
index 0000000..eec1ab7
--- /dev/null
+++ b/contacts/src/main/java/cc/winboll/studio/contacts/phonecallui/PhoneCallActivity.java
@@ -0,0 +1,166 @@
+package cc.winboll.studio.contacts.phonecallui;
+
+import static cc.winboll.studio.contacts.listenphonecall.CallListenerService.formatPhoneNumber;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Build;
+import android.os.Bundle;
+import android.view.View;
+import android.view.WindowManager;
+import android.widget.TextView;
+
+import androidx.annotation.RequiresApi;
+import androidx.appcompat.app.AppCompatActivity;
+
+import cc.winboll.studio.contacts.ActivityStack;
+import cc.winboll.studio.contacts.R;
+
+import java.util.Timer;
+import java.util.TimerTask;
+
+
+/**
+ * 提供接打电话的界面,仅支持 Android M (6.0, API 23) 及以上的系统
+ *
+ * @author aJIEw
+ */
+@RequiresApi(api = Build.VERSION_CODES.M)
+public class PhoneCallActivity extends AppCompatActivity implements View.OnClickListener {
+
+ private TextView tvCallNumberLabel;
+ private TextView tvCallNumber;
+ private TextView tvPickUp;
+ private TextView tvCallingTime;
+ private TextView tvHangUp;
+
+ private PhoneCallManager phoneCallManager;
+ private PhoneCallService.CallType callType;
+ private String phoneNumber;
+
+ private Timer onGoingCallTimer;
+ private int callingTime;
+
+ public static void actionStart(Context context, String phoneNumber,
+ PhoneCallService.CallType callType) {
+ Intent intent = new Intent(context, PhoneCallActivity.class);
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ intent.putExtra(Intent.EXTRA_MIME_TYPES, callType);
+ intent.putExtra(Intent.EXTRA_PHONE_NUMBER, phoneNumber);
+ context.startActivity(intent);
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_phone_call);
+
+ ActivityStack.getInstance().addActivity(this);
+
+ initData();
+
+ initView();
+ }
+
+ private void initData() {
+ phoneCallManager = new PhoneCallManager(this);
+ onGoingCallTimer = new Timer();
+ if (getIntent() != null) {
+ phoneNumber = getIntent().getStringExtra(Intent.EXTRA_PHONE_NUMBER);
+ callType = (PhoneCallService.CallType) getIntent().getSerializableExtra(Intent.EXTRA_MIME_TYPES);
+ }
+ }
+
+ private void initView() {
+ int uiOptions = View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
+ | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION //hide navigationBar
+ | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
+ | View.SYSTEM_UI_FLAG_LAYOUT_STABLE;
+ getWindow().getDecorView().setSystemUiVisibility(uiOptions);
+ getWindow().addFlags(WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON);
+
+ tvCallNumberLabel = findViewById(R.id.tv_call_number_label);
+ tvCallNumber = findViewById(R.id.tv_call_number);
+ tvPickUp = findViewById(R.id.tv_phone_pick_up);
+ tvCallingTime = findViewById(R.id.tv_phone_calling_time);
+ tvHangUp = findViewById(R.id.tv_phone_hang_up);
+
+ tvCallNumber.setText(formatPhoneNumber(phoneNumber));
+ tvPickUp.setOnClickListener(this);
+ tvHangUp.setOnClickListener(this);
+
+ // 打进的电话
+ if (callType == PhoneCallService.CallType.CALL_IN) {
+ tvCallNumberLabel.setText("来电号码");
+ tvPickUp.setVisibility(View.VISIBLE);
+ }
+ // 打出的电话
+ else if (callType == PhoneCallService.CallType.CALL_OUT) {
+ tvCallNumberLabel.setText("呼叫号码");
+ tvPickUp.setVisibility(View.GONE);
+ phoneCallManager.openSpeaker();
+ }
+
+ showOnLockScreen();
+ }
+
+ public void showOnLockScreen() {
+ this.getWindow().setFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON |
+ WindowManager.LayoutParams.FLAG_FULLSCREEN |
+ WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED |
+ WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON,
+ WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON |
+ WindowManager.LayoutParams.FLAG_FULLSCREEN |
+ WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED |
+ WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON);
+ }
+
+ @Override
+ public void onClick(View v) {
+ if (v.getId() == R.id.tv_phone_pick_up) {
+ phoneCallManager.answer();
+ tvPickUp.setVisibility(View.GONE);
+ tvCallingTime.setVisibility(View.VISIBLE);
+ onGoingCallTimer.schedule(new TimerTask() {
+ @Override
+ public void run() {
+ runOnUiThread(new Runnable() {
+ @SuppressLint("SetTextI18n")
+ @Override
+ public void run() {
+ callingTime++;
+ tvCallingTime.setText("通话中:" + getCallingTime());
+ }
+ });
+ }
+ }, 0, 1000);
+ } else if (v.getId() == R.id.tv_phone_hang_up) {
+ phoneCallManager.disconnect();
+ stopTimer();
+ }
+ }
+
+ private String getCallingTime() {
+ int minute = callingTime / 60;
+ int second = callingTime % 60;
+ return (minute < 10 ? "0" + minute : minute) +
+ ":" +
+ (second < 10 ? "0" + second : second);
+ }
+
+ private void stopTimer() {
+ if (onGoingCallTimer != null) {
+ onGoingCallTimer.cancel();
+ }
+
+ callingTime = 0;
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+
+ phoneCallManager.destroy();
+ }
+}
diff --git a/contacts/src/main/java/cc/winboll/studio/contacts/phonecallui/PhoneCallManager.java b/contacts/src/main/java/cc/winboll/studio/contacts/phonecallui/PhoneCallManager.java
new file mode 100644
index 0000000..848a9f5
--- /dev/null
+++ b/contacts/src/main/java/cc/winboll/studio/contacts/phonecallui/PhoneCallManager.java
@@ -0,0 +1,63 @@
+package cc.winboll.studio.contacts.phonecallui;
+
+import android.content.Context;
+import android.media.AudioManager;
+import android.os.Build;
+import android.telecom.Call;
+import android.telecom.VideoProfile;
+
+import androidx.annotation.RequiresApi;
+
+
+@RequiresApi(api = Build.VERSION_CODES.M)
+public class PhoneCallManager {
+
+ public static Call call;
+
+ private Context context;
+ private AudioManager audioManager;
+
+ public PhoneCallManager(Context context) {
+ this.context = context;
+
+ audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
+ }
+
+ /**
+ * 接听电话
+ */
+ public void answer() {
+ if (call != null) {
+ call.answer(VideoProfile.STATE_AUDIO_ONLY);
+ openSpeaker();
+ }
+ }
+
+ /**
+ * 断开电话,包括来电时的拒接以及接听后的挂断
+ */
+ public void disconnect() {
+ if (call != null) {
+ call.disconnect();
+ }
+ }
+
+ /**
+ * 打开免提
+ */
+ public void openSpeaker() {
+ if (audioManager != null) {
+ audioManager.setMode(AudioManager.MODE_IN_CALL);
+ audioManager.setSpeakerphoneOn(true);
+ }
+ }
+
+ /**
+ * 销毁资源
+ */
+ public void destroy() {
+ call = null;
+ context = null;
+ audioManager = null;
+ }
+}
diff --git a/contacts/src/main/java/cc/winboll/studio/contacts/phonecallui/PhoneCallService.java b/contacts/src/main/java/cc/winboll/studio/contacts/phonecallui/PhoneCallService.java
new file mode 100644
index 0000000..0418425
--- /dev/null
+++ b/contacts/src/main/java/cc/winboll/studio/contacts/phonecallui/PhoneCallService.java
@@ -0,0 +1,76 @@
+package cc.winboll.studio.contacts.phonecallui;
+
+import android.os.Build;
+import android.telecom.Call;
+import android.telecom.InCallService;
+
+import androidx.annotation.RequiresApi;
+
+import cc.winboll.studio.contacts.ActivityStack;
+
+
+/**
+ * 监听电话通信状态的服务,实现该类的同时必须提供电话管理的 UI
+ *
+ * @author aJIEw
+ * @see PhoneCallActivity
+ * @see android.telecom.InCallService
+ */
+@RequiresApi(api = Build.VERSION_CODES.M)
+public class PhoneCallService extends InCallService {
+
+ private final Call.Callback callback = new Call.Callback() {
+ @Override
+ public void onStateChanged(Call call, int state) {
+ super.onStateChanged(call, state);
+
+ switch (state) {
+ case Call.STATE_ACTIVE: {
+
+ break;
+ }
+
+ case Call.STATE_DISCONNECTED: {
+ ActivityStack.getInstance().finishActivity(PhoneCallActivity.class);
+ break;
+ }
+
+ }
+ }
+ };
+
+ @Override
+ public void onCallAdded(Call call) {
+ super.onCallAdded(call);
+
+ call.registerCallback(callback);
+ PhoneCallManager.call = call;
+
+ CallType callType = null;
+
+ if (call.getState() == Call.STATE_RINGING) {
+ callType = CallType.CALL_IN;
+ } else if (call.getState() == Call.STATE_CONNECTING) {
+ callType = CallType.CALL_OUT;
+ }
+
+ if (callType != null) {
+ Call.Details details = call.getDetails();
+ String phoneNumber = details.getHandle().getSchemeSpecificPart();
+ PhoneCallActivity.actionStart(this, phoneNumber, callType);
+ }
+ }
+
+ @Override
+ public void onCallRemoved(Call call) {
+ super.onCallRemoved(call);
+
+ call.unregisterCallback(callback);
+ PhoneCallManager.call = null;
+ }
+
+ public enum CallType {
+ CALL_IN,
+ CALL_OUT,
+ }
+}
diff --git a/contacts/src/main/java/cc/winboll/studio/contacts/receivers/MainReceiver.java b/contacts/src/main/java/cc/winboll/studio/contacts/receivers/MainReceiver.java
index 7d93891..f5da4f6 100644
--- a/contacts/src/main/java/cc/winboll/studio/contacts/receivers/MainReceiver.java
+++ b/contacts/src/main/java/cc/winboll/studio/contacts/receivers/MainReceiver.java
@@ -32,8 +32,9 @@ public class MainReceiver extends BroadcastReceiver {
String szAction = intent.getAction();
if (szAction.equals(ACTION_BOOT_COMPLETED)) {
ToastUtils.show("ACTION_BOOT_COMPLETED");
+ MainService.startMainService(context);
} else {
- ToastUtils.show("szAction");
+ ToastUtils.show(szAction);
}
}
@@ -42,7 +43,7 @@ public class MainReceiver extends BroadcastReceiver {
public void registerAction(Context context) {
IntentFilter filter=new IntentFilter();
filter.addAction(ACTION_BOOT_COMPLETED);
- filter.addAction(Intent.ACTION_BATTERY_CHANGED);
+ //filter.addAction(Intent.ACTION_BATTERY_CHANGED);
context.registerReceiver(this, filter);
}
}
diff --git a/contacts/src/main/java/cc/winboll/studio/contacts/receivers/PhoneReceiver.java b/contacts/src/main/java/cc/winboll/studio/contacts/receivers/PhoneReceiver.java
new file mode 100644
index 0000000..d7647a3
--- /dev/null
+++ b/contacts/src/main/java/cc/winboll/studio/contacts/receivers/PhoneReceiver.java
@@ -0,0 +1,34 @@
+package cc.winboll.studio.contacts.receivers;
+
+/**
+ * @Author ZhanGSKen@AliYun.Com
+ * @Date 2025/02/20 17:54:55
+ * @Describe PhoneCallReceiver
+ */
+//import android.content.BroadcastReceiver;
+//import android.content.Context;
+//import android.content.Intent;
+//import android.telephony.TelephonyManager;
+//import android.util.Log;
+//import cc.winboll.studio.contacts.services.PhoneCallService;
+//
+//public class PhoneReceiver extends BroadcastReceiver {
+//
+// public static final String TAG = "PhoneReceiver";
+//
+// @Override
+// public void onReceive(Context context, Intent intent) {
+// if (intent.getAction().equals(TelephonyManager.ACTION_PHONE_STATE_CHANGED)) {
+// String state = intent.getStringExtra(TelephonyManager.EXTRA_STATE);
+// if (state.equals(TelephonyManager.EXTRA_STATE_RINGING)) {
+// String phoneNumber = intent.getStringExtra(TelephonyManager.EXTRA_INCOMING_NUMBER);
+// Log.d(TAG, "Incoming call from: " + phoneNumber);
+//
+// // 启动服务来处理电话接听
+// Intent serviceIntent = new Intent(context, PhoneCallService.class);
+// serviceIntent.putExtra("phoneNumber", phoneNumber);
+// context.startService(serviceIntent);
+// }
+// }
+// }
+//}
diff --git a/contacts/src/main/java/cc/winboll/studio/contacts/services/MainService.java b/contacts/src/main/java/cc/winboll/studio/contacts/services/MainService.java
index efaf13e..40cf20f 100644
--- a/contacts/src/main/java/cc/winboll/studio/contacts/services/MainService.java
+++ b/contacts/src/main/java/cc/winboll/studio/contacts/services/MainService.java
@@ -11,6 +11,7 @@ package cc.winboll.studio.contacts.services;
* https://blog.csdn.net/cyp331203/article/details/38920491
*/
import android.app.Service;
+import cc.winboll.studio.contacts.listenphonecall.CallListenerService;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
@@ -97,7 +98,8 @@ public class MainService extends Service {
mMainReceiver = new MainReceiver(this);
mMainReceiver.registerAction(this);
}
-
+
+ startPhoneCallListener();
MainServiceThread.getInstance(this, mMainServiceHandler).start();
@@ -125,6 +127,11 @@ public class MainService extends Service {
// LogUtils.d(TAG, "startService(intent)");
// bindService(new Intent(this, AssistantService.class), mMyServiceConnection, Context.BIND_IMPORTANT);
}
+
+ void startPhoneCallListener() {
+ Intent callListener = new Intent(this, CallListenerService.class);
+ startService(callListener);
+ }
@Override
public void onDestroy() {
diff --git a/contacts/src/main/java/cc/winboll/studio/contacts/services/PhoneCallService.java b/contacts/src/main/java/cc/winboll/studio/contacts/services/PhoneCallService.java
new file mode 100644
index 0000000..5a3921a
--- /dev/null
+++ b/contacts/src/main/java/cc/winboll/studio/contacts/services/PhoneCallService.java
@@ -0,0 +1,75 @@
+package cc.winboll.studio.contacts.services;
+
+/**
+ * @Author ZhanGSKen@AliYun.Com
+ * @Date 2025/02/20 19:58:02
+ * @Describe MyPhoneCallService
+ */
+import android.telecom.Call;
+import android.telecom.InCallService;
+import cc.winboll.studio.contacts.PhoneCallManager;
+
+public class PhoneCallService extends InCallService {
+
+ public static final String TAG = "PhoneCallService";
+
+ private Call.Callback callback = new Call.Callback() {
+ @Override
+ public void onStateChanged(Call call, int state) {
+ super.onStateChanged(call, state);
+ switch (state) {
+ case Call.STATE_ACTIVE: {
+ break; // 通话中
+ }
+ case Call.STATE_DISCONNECTED: {
+ break; // 通话结束
+ }
+ }
+ }
+ };
+
+// @Override
+// public void onCallAdded(Call call) {
+// super.onCallAdded(call);
+//
+// call.registerCallback(callback);
+// }
+//
+// @Override
+// public void onCallRemoved(Call call) {
+// super.onCallRemoved(call);
+//
+// call.unregisterCallback(callback);
+// }
+
+ @Override
+ public void onCallAdded(Call call) {
+ super.onCallAdded(call);
+
+ call.registerCallback(callback);
+ PhoneCallManager.call = call; // 传入call
+
+// CallType callType = null;
+//
+// if (call.getState() == Call.STATE_RINGING) {
+// callType = CallType.CALL_IN;
+// } else if (call.getState() == Call.STATE_CONNECTING) {
+// callType = CallType.CALL_OUT;
+// }
+//
+// if (callType != null) {
+// Call.Details details = call.getDetails();
+// String phoneNumber = details.getHandle().toString().substring(4)
+// .replaceAll("%20", ""); // 去除拨出电话中的空格
+// PhoneCallActivity.actionStart(this, phoneNumber, callType);
+// }
+ }
+
+ @Override
+ public void onCallRemoved(Call call) {
+ super.onCallRemoved(call);
+
+ call.unregisterCallback(callback);
+ PhoneCallManager.call = null;
+ }
+}
diff --git a/contacts/src/main/res/drawable/ic_phone_call_in.xml b/contacts/src/main/res/drawable/ic_phone_call_in.xml
new file mode 100644
index 0000000..792377d
--- /dev/null
+++ b/contacts/src/main/res/drawable/ic_phone_call_in.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/contacts/src/main/res/drawable/ic_phone_call_out.xml b/contacts/src/main/res/drawable/ic_phone_call_out.xml
new file mode 100644
index 0000000..833d25a
--- /dev/null
+++ b/contacts/src/main/res/drawable/ic_phone_call_out.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/contacts/src/main/res/drawable/ic_phone_hang_up.xml b/contacts/src/main/res/drawable/ic_phone_hang_up.xml
new file mode 100644
index 0000000..98b57b2
--- /dev/null
+++ b/contacts/src/main/res/drawable/ic_phone_hang_up.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/contacts/src/main/res/drawable/ic_phone_pick_up.xml b/contacts/src/main/res/drawable/ic_phone_pick_up.xml
new file mode 100644
index 0000000..23080f1
--- /dev/null
+++ b/contacts/src/main/res/drawable/ic_phone_pick_up.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/contacts/src/main/res/layout/activity_dialer.xml b/contacts/src/main/res/layout/activity_dialer.xml
new file mode 100644
index 0000000..dc18c88
--- /dev/null
+++ b/contacts/src/main/res/layout/activity_dialer.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
diff --git a/contacts/src/main/res/layout/activity_phone_call.xml b/contacts/src/main/res/layout/activity_phone_call.xml
new file mode 100644
index 0000000..9768e9f
--- /dev/null
+++ b/contacts/src/main/res/layout/activity_phone_call.xml
@@ -0,0 +1,95 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/contacts/src/main/res/layout/activity_settings.xml b/contacts/src/main/res/layout/activity_settings.xml
new file mode 100644
index 0000000..b77a699
--- /dev/null
+++ b/contacts/src/main/res/layout/activity_settings.xml
@@ -0,0 +1,46 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/contacts/src/main/res/layout/view_phone_call.xml b/contacts/src/main/res/layout/view_phone_call.xml
new file mode 100644
index 0000000..5036c76
--- /dev/null
+++ b/contacts/src/main/res/layout/view_phone_call.xml
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/contacts/src/main/res/menu/toolbar_main.xml b/contacts/src/main/res/menu/toolbar_main.xml
index 881e169..d248d27 100644
--- a/contacts/src/main/res/menu/toolbar_main.xml
+++ b/contacts/src/main/res/menu/toolbar_main.xml
@@ -5,5 +5,8 @@
-
+
+
diff --git a/contacts/src/main/res/values/styles.xml b/contacts/src/main/res/values/styles.xml
index 4b743f7..8380acf 100644
--- a/contacts/src/main/res/values/styles.xml
+++ b/contacts/src/main/res/values/styles.xml
@@ -1,6 +1,11 @@