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 @@ + + + + + +