Как заменить экран входящих вызовов на Android?

1,00
р.
Дано: есть приложение Ultimate Call Screen HD - аналогичных приложений на маркете довольно много.
Вопрос: как это работает? Как написано?
У кого какие идеи?
P.S. Ориентируемся на нерутованные аппараты.

Ответ
TL DR Приложение представляется системе гарнитурой для того, чтобы принимать/завершать звонки
У приложения Ultimate Call Screen HD в манифесте указано следущее:
...
Из манифеста видно, что PhoneReceiver получает первым интенты от системы про исходящий/входящий звонок. Далее он делегирует обработку сервисам InCallService и OutcallService, которые отображают необходимый интерфейс для управления и используют AudioManager для передачи голоса
UPDATE: Откроем класс InCallService
public class InCallService extends Service implements SensorEventListener, a, b, e { ... static boolean H WindowManager G WindowManager.LayoutParams I CallWindowView J ... private void e(final boolean b) { if (b) { return } this.G.addView((View)this.J, (ViewGroup$LayoutParams)this.I) InCallService.H = true }
private void f(final boolean b) { if (!b) { return } if (this.J.getWindowToken() != null) { this.G.removeView((View)this.J) } InCallService.H = false }
private void t() { this.I = new WindowManager.LayoutParams(-1, -1, 2010, 2621600, -1) if (g.a("hide_status_bar", true)) { final WindowManager.LayoutParams i = this.I i.flags |= 0x100 } else { this.I.type = 2003 } this.I.gravity = 80 if (g.a("force_full_brightness", true)) { this.I.screenBrightness = 1.0f } (this.J = (CallWindowView)((LayoutInflater)this.getSystemService("layout_inflater")).inflate(2130903099, (ViewGroup)null)).a(new a(this)) this.G = (WindowManager)this.getSystemService("window") } ... }
Из этого кода видно, что приложение накрывает стандартное окно звонилки своим CallWindowView (для этого и необходимо разрешение android.permission.SYSTEM_ALERT_WINDOW). Разметка для входящего звонка хранится в R.layout.two_button_frame = 2130903099
<?xml version="1.0" encoding="utf-8"?> ... ... ...
R.id.answerbutton = 2131558590 R.id.rejectbutton = 2131558495 В сервисе есть код:
public void onCreate() { ... this.t() this.at = (AudioManager)this.al.getSystemService("audio") ... this.f = (Button)this.J.findViewById(2131558495) this.e = (Button)this.J.findViewById(2131558590) ... this.J.setOnTouchListener((View$OnTouchListener)new ac(this)) this.J.setOnKeyListener((View$OnKeyListener)new ad(this)) this.f.setOnClickListener((View$OnClickListener)new ae(this)) this.f.setOnLongClickListener((View$OnLongClickListener)new af(this)) this.e.setOnClickListener((View$OnClickListener)new b(this)) this.e.setOnLongClickListener((View$OnLongClickListener)new c(this)) ... }
При нажатии кнопки "Принять вызов" срабатывает слушатель:
class b implements View$OnClickListener { final InCallService a
b(final InCallService a) { super() this.a = a }
public void onClick(final View view) { ... if (this.a.a(this.a.al)) { this.a.ak.a(this.a.al) ... } else { final Intent intent = new Intent("android.intent.action.VIEW") intent.setData(Uri.parse("market://details?id=com.lowveld.ucshdlicense")) intent.setFlags(268435456) this.a.startActivity(intent) this.a.d() } ... } }
Который передает управление в данный класс:
public class a { Boolean a Boolean b
public a() { super() this.a = false this.b = false }
private void a(final Context context, final int n, final boolean b) { new Thread(new b(this, n, b, context)).start() }
private void a(final Context context, final boolean b) { this.a = g.a("isHeadsetOn", false) this.b = ((AudioManager)context.getSystemService("audio")).isWiredHeadsetOn() while (true) { Label_0065: { if (!g.a("root_activate_answer", false)) { //можно ли использовать рут-права break Label_0065 } try { this.b() final int n = 0 if (n != 0) { this.b(context, b) //рут-права отсутствуют/нельзя использовать } return } catch (Exception ex) { final int n = 1 continue } } final int n = 1 continue } }
private void b(final Context context, final boolean b) { if (context != null) { if (!j.b()) { API < 16 final Intent intent = new Intent("android.intent.action.HEADSET_PLUG") intent.addFlags(1073741824) intent.putExtra("state", 2) intent.putExtra("name", "Headset") context.sendOrderedBroadcast(intent, (String)null) } final Intent intent2 = new Intent("android.intent.action.MEDIA_BUTTON") intent2.putExtra("android.intent.extra.KEY_EVENT", (Parcelable)new KeyEvent(0, 79)) // шлем действие ACTION_DOWN с кодом KEYCODE_HEADSETHOOK context.sendBroadcast(intent2, "android.permission.CALL_PRIVILEGED") final Intent intent3 = new Intent("android.intent.action.MEDIA_BUTTON") intent3.putExtra("android.intent.extra.KEY_EVENT", (Parcelable)new KeyEvent(1, 79)) // шлем действие ACTION_UP с кодом KEYCODE_HEADSETHOOK context.sendBroadcast(intent3, "android.permission.CALL_PRIVILEGED") if (!this.a && !this.b && !j.b()) { final Intent intent4 = new Intent("android.intent.action.HEADSET_PLUG") intent4.addFlags(1073741824) intent4.putExtra("state", 0) intent4.putExtra("name", "Headset") context.sendOrderedBroadcast(intent4, (String)null) } if (b && j.b()) { return } } }
void a() { try { final DataOutputStream dataOutputStream = new DataOutputStream(Runtime.getRuntime().exec("su").getOutputStream()) dataOutputStream.writeBytes("input keyevent " + Integer.toString(6) + "
") dataOutputStream.writeBytes("exit
") dataOutputStream.flush() dataOutputStream.close() } catch (Exception ex) { ex.printStackTrace() throw ex } }
public void a(final Context context) { //вызывается из слушателя if (j.a()) { // API >= 21 this.a(context, 79, true) return } this.a(context, true) }
void b() { try { final DataOutputStream dataOutputStream = new DataOutputStream(Runtime.getRuntime().exec("su").getOutputStream()) dataOutputStream.writeBytes("input keyevent " + Integer.toString(5) + "
") //поднимает трубку dataOutputStream.writeBytes("exit
") dataOutputStream.flush() dataOutputStream.close() } catch (Exception ex) { ex.printStackTrace() throw ex } } ... }