着信通話用のカスタム画面を作成しようとしているので、着信通話にプログラムで応答しようとしています。次のコードを使用していますが、Android 5.0では機能しません。
// Simulate a press of the headset button to pick up the call
Intent buttonDown = new Intent(Intent.ACTION_MEDIA_BUTTON);
buttonDown.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_HEADSETHOOK));
context.sendOrderedBroadcast(buttonDown, "Android.permission.CALL_PRIVILEGED");
// froyo and beyond trigger on buttonUp instead of buttonDown
Intent buttonUp = new Intent(Intent.ACTION_MEDIA_BUTTON);
buttonUp.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_HEADSETHOOK));
context.sendOrderedBroadcast(buttonUp, "Android.permission.CALL_PRIVILEGED");
質問はもともとAndroid Lのサポートを求めていたにもかかわらず、人々はこの質問と回答にまだ答えているようですので、Android 8.0 Oreoで導入された改善点を説明する価値があります。下位互換性のあるメソッドについては、引き続き以下で説明します。
Android 8.0 Oreo で始まる、 PHONE許可グループ には ANSWER_PHONE_CALLS許可 。許可の名前が示すように、それを保持することで、アプリケーションは、リフレクションを使用したり、ユーザーをシミュレートしたりしてシステムをハッキングすることなく、適切なAPI呼び出しを通じて着信呼び出しをプログラムで受け入れることができます。
実行時にシステムバージョンを確認する必要があります 古いAndroidバージョンをサポートしている場合、古いAndroidバージョンのサポートを維持しながらこの新しいAPI呼び出しをカプセル化できます。 実行時に許可を要求する に従って、実行時に新しいAndroidバージョンの標準となる新しい許可を取得する必要があります。
許可を得た後、アプリは単に TelecomManagerのacceptRingingCall メソッドを呼び出すだけです。基本的な呼び出しは、次のようになります。
TelecomManager tm = (TelecomManager) mContext
.getSystemService(Context.TELECOM_SERVICE);
if (tm == null) {
// whether you want to handle this is up to you really
throw new NullPointerException("tm == null");
}
tm.acceptRingingCall();
デバイスを無制限に制御できる場合
隠された内部メソッドであるTelephonyManager.answerRingingCall()があります。これは、インターウェブで議論されたITelephony.answerRingingCall()のブリッジとして機能し、最初は有望だと思われます。 not4.4.2_r1で利用可能 コミットでのみ導入されたため 83da75d for Android 4.4 KitKat( 4.4.3_r1の1537行目 )以降、コミットで「再導入」 f1e1e77 Lollipop( 5.0.0_r1の3138行目 ) Gitツリーの構造。つまり、Lollipopを搭載したデバイスのみをサポートしている場合を除きます。Lollipopは、現時点でのわずかな市場シェアに基づいた決定である可能性がありますが、このルートを下る場合はフォールバックメソッドを提供する必要があります。
問題のメソッドはSDKアプリケーションの使用から隠されているため、 reflection を使用して、実行時にメソッドを動的に調べて使用する必要があります。リフレクションに慣れていない場合は、すぐに読むことができます リフレクションとは何ですか、なぜ役立つのですか? 。 Trail:Reflection API で詳細を掘り下げることもできます。
// set the logging tag constant; you probably want to change this
final String LOG_TAG = "TelephonyAnswer";
TelephonyManager tm = (TelephonyManager) mContext
.getSystemService(Context.TELEPHONY_SERVICE);
try {
if (tm == null) {
// this will be easier for debugging later on
throw new NullPointerException("tm == null");
}
// do reflection magic
tm.getClass().getMethod("answerRingingCall").invoke(tm);
} catch (Exception e) {
// we catch it all as the following things could happen:
// NoSuchMethodException, if the answerRingingCall() is missing
// SecurityException, if the security manager is not happy
// IllegalAccessException, if the method is not accessible
// IllegalArgumentException, if the method expected other arguments
// InvocationTargetException, if the method threw itself
// NullPointerException, if something was a null value along the way
// ExceptionInInitializerError, if initialization failed
// something more crazy, if anything else breaks
// TODO decide how to handle this state
// you probably want to set some failure state/go to fallback
Log.e(LOG_TAG, "Unable to use the Telephony Manager directly.", e);
}
実際、わずかな問題が1つあります。このメソッドは完全に機能する必要がありますが、セキュリティマネージャーは、呼び出し元が Android.permission.MODIFY_PHONE_STATE を保持することを望んでいます。この許可は、サードパーティがそれに触れることは想定されていないため、システムの機能の一部しか文書化されていません(その文書からわかるように)。 <uses-permission>
を追加してみることができますが、この許可の保護レベルはsignature | system( 行1201を参照してください。 5.0.0_r1のcore/AndroidManifestの )。
Issue 34785:Update Android:protectionLevel documentation は2012年に作成されたもので、特定の「パイプ構文」に関する詳細が欠落していることを確認できますが、試してみると、 「AND」は、許可が付与されるために、指定されたすべてのフラグが満たされる必要があることを意味します。この仮定の下で作業すると、アプリケーションが必要になります。
システムアプリケーションとしてインストールされます。
これは問題ないはずであり、Zipを使用してインストールするようにユーザーに依頼することで達成できます。たとえば、まだパッケージ化されていないカスタムROMにGoogleアプリをルート化またはインストールする場合です。
frameworks/base aka system、aka ROMと同じ署名で署名されています。
これが問題が発生する場所です。これを行うには、フレームワーク/ベースに署名するために使用されるキーを手にする必要があります。 Nexusのファクトリイメージ用のGoogleのキーにアクセスする必要があるだけでなく、他のすべてのOEMおよびROM開発者のキーにもアクセスする必要があります。これはもっともらしいとは思えないので、カスタムROMを作成してユーザーに切り替えを要求する(これは難しいかもしれません)か、アクセス許可保護を利用するエクスプロイトを見つけることにより、システムキーでアプリケーションに署名させることができますレベルはバイパスすることができます(これも難しいかもしれません)。
さらに、この動作は 問題34792に関連しているようです:Android Jelly bean/4.1:Android.permission.READ_LOGSは機能しなくなりました 文書化されていない開発フラグとともに同じ保護レベルを利用します。
TelephonyManagerでの作業は良いように聞こえますが、実際にはそれほど簡単ではない適切な許可を取得しない限り機能しません。
悲しいことに、クールなツールを使用するために Android.permission.MODIFY_PHONE_STATE を保持する必要があるように思われます。これは、これらのメソッドにアクセスするのに苦労することを意味します。
デバイスで実行されているビルドが指定されたコードで動作することをテストできる場合。
TelephonyManagerと対話することなく、service
実行可能ファイルを介してサービスと対話する可能性もあります。
これはかなり単純ですが、このルートに関するドキュメントは他のドキュメントよりもさらに少なくなっています。実行可能ファイルは、サービス名とコードという2つの引数を確実に取ります。
使用したいサービス名はphoneです。
これは、service list
を実行することで確認できます。
使用したいcodeは6だったようですが、今は5。
現在、多くのバージョンで IBinder.FIRST_CALL_TRANSACTION + 5に基づいているようです( 1.5_r4 から 4.4.4_r1 )。ただし、コードのローカルテスト中です。 5は着信コールに応答するために働きました。 Lollipoはあらゆる面で大規模な更新であるため、ここでも変更された内部構造が理解できます。
これは、service call phone 5
のコマンドで発生します。
次のコードは、概念実証として機能するように作られた大まかな実装です。実際に先に進んでこの方法を使用する場合は、 問題のないsuの使用に関するガイドライン を確認し、おそらくより完全に開発された libsuperuser by チェーンファイア 。
try {
Process proc = Runtime.getRuntime().exec("su");
DataOutputStream os = new DataOutputStream(proc.getOutputStream());
os.writeBytes("service call phone 5\n");
os.flush();
os.writeBytes("exit\n");
os.flush();
if (proc.waitFor() == 255) {
// TODO handle being declined root access
// 255 is the standard code for being declined root for SU
}
} catch (IOException e) {
// TODO handle I/O going wrong
// this probably means that the device isn't rooted
} catch (InterruptedException e) {
// don't swallow interruptions
Thread.currentThread().interrupt();
}
<!-- Inform the user we want them root accesses. -->
<uses-permission Android:name="Android.permission.ACCESS_SUPERUSER"/>
悲しいことに、そうです。その上で Runtime.exec を使用してみることはできますが、そのルートで運を得ることができませんでした。
聞いてくれてうれしいです。文書化されていないため、上記の見かけ上のコードの違いが示すように、これはさまざまなバージョンにまたがる可能性があります。サービス名はおそらくさまざまなビルド全体でphoneのままである必要がありますが、コード値は同じバージョンの複数のビルド間で変更できます(たとえば、OEM肌)順番に使用される方法を破る。したがって、テストがNexus 4(mako/occam)で行われたことに言及する価値があります。私は個人的にこの方法を使用することをお勧めしますが、より安定した方法を見つけることができないので、これがベストショットだと思います。
落ち着かなければならない時のために。
次のセクションは、 この回答に強く影響を受けましたライリーC 。
元の質問で投稿されたシミュレートされたヘッドセットのインテントメソッドは、予想どおりに放送されているように見えますが、通話に応答するという目標を達成するようには見えません。これらのインテントを処理するコードが適切に配置されているように見えますが、それらは単に気にかけられていないため、この方法に対する何らかの新しい対策が必要になります。ログにも関心のあるものは何も表示されません。個人的には、Androidのソースを掘り下げることは、Googleがわずかな変更を導入して、とにかく使用する方法を簡単に破る可能性があるためです。
この動作は、入力実行可能ファイルを使用して一貫して再現できます。これは、キーコード引数を取ります。これには、単に KeyEvent.KEYCODE_HEADSETHOOK を渡します。このメソッドはルートアクセスを必要としないため、一般の一般的なユースケースに適していますが、メソッドには小さな欠点があります-ヘッドセットボタンを押すイベントを許可を要求するように指定することはできません。ボタンを押してチェーン全体にバブルアップするため、ボタンを押すタイミングをシミュレートするタイミングに注意する必要があります。たとえば、優先度の高い他のユーザーが処理する準備ができていない場合、音楽プレーヤーをトリガーして再生を開始します行事。
new Thread(new Runnable() {
@Override
public void run() {
try {
Runtime.getRuntime().exec("input keyevent " +
Integer.toString(KeyEvent.KEYCODE_HEADSETHOOK));
} catch (IOException e) {
// Runtime.exec(String) had an I/O problem, try to fall back
String enforcedPerm = "Android.permission.CALL_PRIVILEGED";
Intent btnDown = new Intent(Intent.ACTION_MEDIA_BUTTON).putExtra(
Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN,
KeyEvent.KEYCODE_HEADSETHOOK));
Intent btnUp = new Intent(Intent.ACTION_MEDIA_BUTTON).putExtra(
Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_UP,
KeyEvent.KEYCODE_HEADSETHOOK));
mContext.sendOrderedBroadcast(btnDown, enforcedPerm);
mContext.sendOrderedBroadcast(btnUp, enforcedPerm);
}
}
}).start();
Android 8.0 Oreo以降にはニースのパブリックAPIがあります。
Android 8.0 Oreoより前の公開APIはありません。内部APIは立ち入り禁止であるか、単にドキュメントがありません。注意して進める必要があります。
動作させるには、コードが実行されるロック画面に(非表示の)アクティビティを表示する必要があります。
<uses-permission Android:name="Android.permission.WAKE_LOCK" />
<uses-permission Android:name="Android.permission.DISABLE_KEYGUARD" />
<activity Android:name="com.mysms.Android.lib.activity.AcceptCallActivity"
Android:launchMode="singleTop"
Android:excludeFromRecents="true"
Android:taskAffinity=""
Android:configChanges="orientation|keyboardHidden|screenSize"
Android:theme="@style/Mysms.Invisible">
</activity>
package com.mysms.Android.lib.activity;
import Android.app.Activity;
import Android.app.KeyguardManager;
import Android.content.BroadcastReceiver;
import Android.content.Context;
import Android.content.Intent;
import Android.content.IntentFilter;
import Android.media.AudioManager;
import Android.os.Build;
import Android.os.Bundle;
import Android.telephony.TelephonyManager;
import Android.view.KeyEvent;
import Android.view.WindowManager;
import org.Apache.log4j.Logger;
import Java.io.IOException;
public class AcceptCallActivity extends Activity {
private static Logger logger = Logger.getLogger(AcceptCallActivity.class);
private static final String MANUFACTURER_HTC = "HTC";
private KeyguardManager keyguardManager;
private AudioManager audioManager;
private CallStateReceiver callStateReceiver;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
keyguardManager = (KeyguardManager) getSystemService(Context.KEYGUARD_SERVICE);
audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
}
@Override
protected void onResume() {
super.onResume();
registerCallStateReceiver();
updateWindowFlags();
acceptCall();
}
@Override
protected void onPause() {
super.onPause();
if (callStateReceiver != null) {
unregisterReceiver(callStateReceiver);
callStateReceiver = null;
}
}
private void registerCallStateReceiver() {
callStateReceiver = new CallStateReceiver();
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(TelephonyManager.ACTION_PHONE_STATE_CHANGED);
registerReceiver(callStateReceiver, intentFilter);
}
private void updateWindowFlags() {
if (keyguardManager.inKeyguardRestrictedInputMode()) {
getWindow().addFlags(
WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD |
WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON |
WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED);
} else {
getWindow().clearFlags(
WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD |
WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON |
WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED);
}
}
private void acceptCall() {
// for HTC devices we need to broadcast a connected headset
boolean broadcastConnected = MANUFACTURER_HTC.equalsIgnoreCase(Build.MANUFACTURER)
&& !audioManager.isWiredHeadsetOn();
if (broadcastConnected) {
broadcastHeadsetConnected(false);
}
try {
try {
logger.debug("execute input keycode headset hook");
Runtime.getRuntime().exec("input keyevent " +
Integer.toString(KeyEvent.KEYCODE_HEADSETHOOK));
} catch (IOException e) {
// Runtime.exec(String) had an I/O problem, try to fall back
logger.debug("send keycode headset hook intents");
String enforcedPerm = "Android.permission.CALL_PRIVILEGED";
Intent btnDown = new Intent(Intent.ACTION_MEDIA_BUTTON).putExtra(
Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN,
KeyEvent.KEYCODE_HEADSETHOOK));
Intent btnUp = new Intent(Intent.ACTION_MEDIA_BUTTON).putExtra(
Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_UP,
KeyEvent.KEYCODE_HEADSETHOOK));
sendOrderedBroadcast(btnDown, enforcedPerm);
sendOrderedBroadcast(btnUp, enforcedPerm);
}
} finally {
if (broadcastConnected) {
broadcastHeadsetConnected(false);
}
}
}
private void broadcastHeadsetConnected(boolean connected) {
Intent i = new Intent(Intent.ACTION_HEADSET_PLUG);
i.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY);
i.putExtra("state", connected ? 1 : 0);
i.putExtra("name", "mysms");
try {
sendOrderedBroadcast(i, null);
} catch (Exception e) {
}
}
private class CallStateReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
finish();
}
}
}
<style name="Mysms.Invisible">
<item name="Android:windowFrame">@null</item>
<item name="Android:windowBackground">@Android:color/transparent</item>
<item name="Android:windowContentOverlay">@null</item>
<item name="Android:windowNoTitle">true</item>
<item name="Android:windowIsTranslucent">true</item>
<item name="Android:windowAnimationStyle">@null</item>
</style>
Intent intent = new Intent(context, AcceptCallActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK
| Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
context.startActivity(intent);
以下は私のために働いた代替アプローチです。 MediaController APIを使用して、キーイベントをテレコムサーバーに直接送信します。これには、アプリに BIND_NOTIFICATION_LISTENER_SERVICE 許可が必要であり、にユーザーからの通知アクセスの明示的な許可が与えられている必要があります。
@TargetApi(Build.VERSION_CODES.Lollipop)
void sendHeadsetHookLollipop() {
MediaSessionManager mediaSessionManager = (MediaSessionManager) getApplicationContext().getSystemService(Context.MEDIA_SESSION_SERVICE);
try {
List<MediaController> mediaControllerList = mediaSessionManager.getActiveSessions
(new ComponentName(getApplicationContext(), NotificationReceiverService.class));
for (MediaController m : mediaControllerList) {
if ("com.Android.server.telecom".equals(m.getPackageName())) {
m.dispatchMediaButtonEvent(new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_HEADSETHOOK));
log.info("HEADSETHOOK sent to telecom server");
break;
}
}
} catch (SecurityException e) {
log.error("Permission error. Access to notification not granted to the app.");
}
}
上記のコードのNotificationReceiverService.class
は、空のクラスである可能性があります。
import Android.service.notification.NotificationListenerService;
public class NotificationReceiverService extends NotificationListenerService{
public NotificationReceiverService() {
}
}
マニフェストの対応するセクションで:
<service Android:name=".NotificationReceiverService" Android:permission="Android.permission.BIND_NOTIFICATION_LISTENER_SERVICE"
Android:enabled="true" Android:exported="true">
<intent-filter>
<action Android:name="Android.service.notification.NotificationListenerService" />
</intent-filter>
イベントのターゲットは明示的であるため、メディアプレーヤーをトリガーする副作用はおそらく回避されるはずです。
注:呼び出しイベントの直後にテレコムサーバーがアクティブにならない場合があります。これを確実に機能させるには、アプリが MediaSessionManager.OnActiveSessionsChangedListener を実装して、イベントを送信する前にテレコムサーバーがアクティブになるタイミングを監視すると便利です。
更新:
Android Oでは、ACTION_DOWN
の前にACTION_UP
をシミュレートする必要があります。そうでない場合、上記の効果はありません。すなわち、以下が必要です:
m.dispatchMediaButtonEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_HEADSETHOOK));
m.dispatchMediaButtonEvent(new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_HEADSETHOOK));
ただし、Android O(トップアンサーを参照)以降、呼び出しに応答するための公式の呼び出しが利用できるため、Android O.
@Muzikantの答えを少し詳しく説明し、デバイスを少しきれいにするために少し変更するには、 KeyEvent.KEYCODE_HEADSETHOOK の定数であるinput keyevent 79
を試してください。 非常に大まかに:
new Thread(new Runnable() {
@Override
public void run() {
try {
Runtime.getRuntime().exec( "input keyevent " + KeyEvent.KEYCODE_HEADSETHOOK );
}
catch (Throwable t) {
// do something proper here.
}
}
}).start();
かなり悪いコーディング規則を許して、私はRuntime.exec()呼び出しにあまり精通していません。私のデバイスはルート化されておらず、ルート権限も要求していないことに注意してください。
このアプローチの問題は、特定の条件下でのみ機能することです(私にとって)。つまり、呼び出しが鳴っている間にユーザーが選択したメニューオプションから上記のスレッドを実行すると、呼び出しは正常に応答します。着信状態を監視するレシーバーから実行すると、完全に無視されます。
そのため、Nexus 5では、ユーザー主導の応答に最適であり、カスタムコール画面の目的に適しているはずです。あらゆる種類の自動化されたコール制御タイプのアプリケーションでは機能しません。
また、これもまた、おそらく1つまたは2つのアップデートで動作しなくなることを含め、考えられるすべての警告です。
おかげで、@ notzのおかげでLollipopの作業ができました。このコードを古いAndroid SDKで動作させるには、次のcodを実行できます。
if (Build.VERSION.SDK_INT >= 21) {
Intent answerCalintent = new Intent(context, AcceptCallActivity.class);
answerCalintent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK |
Intent.FLAG_ACTIVITY_CLEAR_TASK |
Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
context.startActivity(answerCalintent);
}
else {
if (telephonyService != null) {
try {
telephonyService.answerRingingCall();
}
catch (Exception e) {
answerPhoneHeadsethook();
}
}
}
adbコマンド経由 adbでコールをピックアップする方法
AndroidはLinuxであり、フロントエンドに大規模なJVMがあることに注意してください。コマンドラインアプリをダウンロードして電話のルートを設定すると、通常のLinuxコンピューターと通常のすべてを実行するコマンドラインができます。スクリプトを実行し、sshすることもできます(OpenVPNのトリック)
通話に自動的に応答した後にスピーカーフォンをオンにする方法
上記の問題をsetSpeakerphoneOnで解決しました。電話に自動応答するためのユースケースでは、スピーカーフォンも役立つことが多いため、ここに投稿する価値があると思います。このスレッドの全員に感謝します。なんて素晴らしい仕事です。
これは、ROOTなしのNexus 4のAndroid 5.1.1で機能します。 ;)
必要な許可:
<uses-permission Android:name="Android.permission.MODIFY_AUDIO_SETTINGS"/>
Javaコード:
// this means the phone has answered
if(state==TelephonyManager.CALL_STATE_OFFHOOK)
{
// try and turn on speaker phone
final Handler mHandler = new Handler();
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
AudioManager audioManager = (AudioManager) localContext.getSystemService(Context.AUDIO_SERVICE);
// this doesnt work without Android.permission.MODIFY_PHONE_STATE
// audioManager.setMode(AudioManager.MODE_IN_CALL);
// weirdly this works
audioManager.setMode(AudioManager.MODE_NORMAL); // this is important
audioManager.setSpeakerphoneOn(true);
// note the phone interface won't show speaker phone is enabled
// but the phone speaker will be on
// remember to turn it back off when your done ;)
}
}, 500); // half a second delay is important or it might fail
}