1. ホーム
  2. android

[解決済み] Android 5.0(Lollipop)で着信にプログラムで応答するには?

2023-03-19 18:43:01

質問

着信用のカスタム画面を作成しようとしているのですが、プログラム的に着信に応答するようにしようとしています。以下のコードを使用していますが、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 8.0 Oreoにアップデートする

元々Android L対応の質問だったにも関わらず、未だにこのQ&Aを叩く人がいるようなので、Android 8.0 Oreoで導入された改善点を記述しておきます。下位互換の方法については、やはり以下に記述します。

何が変わったのですか?

で始まる Android 8.0 Oreo は、その 携帯電話 許可グループ にも 電話に出る 許可 . パーミッションの名前が示すように、それを保持することで、リフレクションを使用してシステムの周りをハッキングしたり、ユーザーをシミュレートすることなく、アプリが適切なAPIコールを通じてプログラム的に着信コールを受け入れることができるようになります。

この変更をどのように利用するか?

あなたは 実行時にシステムバージョンをチェックする を実行し、古い 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();


方法1:TelephonyManager.answerRingingCall()

デバイスを無制限にコントロールできる場合のために。

これは何ですか?

TelephonyManager.answerRingingCall()があり、これは隠れた内部メソッドです。これは、インターウェブで議論されているITelephony.answerRingingCall()のブリッジとして動作し、最初は有望だと思われます。それは ではなく で利用可能です。 4.4.2_r1 で導入されたので、コミット 83da75d で、Android 4.4 KitKat ( 4.4.3_r1 の 1537 行目 ) で、その後コミットで "reintroduced" されました。 f1e1e77 をロリポップ用 ( 5.0.0_r1 の 3138 行目 というように、Git ツリーがどのように構成されているかに起因しています。つまり、Lollipop を搭載したデバイスのみをサポートするのでなければ、現時点での Lollipop の市場シェアが小さいことを考えると、おそらく悪い決断ですが、このルートを進む場合、代替手段を提供する必要があるということです。

これをどのように使用するのでしょうか?

問題のメソッドは、SDKアプリケーションの使用では隠されているので リフレクション を使用して、実行時にメソッドを動的に調べ、使用する必要があります。リフレクションに馴染みがない場合は、すぐに リフレクションとは何か、そしてなぜ便利なのか? . また、次のサイトで詳細を知ることができます。 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> を追加することもできますが、このパーミッションの保護レベルは 署名|システム ( 5.0.0_r1 の core/AndroidManifest の 1201 行目を参照してください。 ).

を読むことができます。 34785号: android:protectionLevel ドキュメントを更新しました。 を読むと、具体的な "pipe syntax" についての詳細が不明であることがわかりますが、いろいろ試してみたところ、許可を与えるためには、指定したフラグがすべて満たされなければならないという意味の「AND」として機能しなければならないようです。その前提で作業すると、アプリケーションが必要ということになります。

  1. システムアプリケーションとしてインストールされます。

    これは問題ないはずで、root 化の際や、すでにパッケージ化されていないカスタム ROM に Google アプリをインストールする際など、リカバリで ZIP を使用してインストールするようにユーザーに求めることで実現できます。

  2. frameworks/base aka the system, aka the ROMと同じ署名がされています。

    ここで問題が出てきます。これを行うには、フレームワーク/ベースに署名するために使用されるキーを手に入れる必要があります。Nexus ファクトリー イメージ用の Google のキーにアクセスするだけでなく、他のすべての OEM や ROM 開発者のキーにもアクセスする必要があります。これはもっともらしくないので、カスタム ROM を作成し、ユーザーにそれに切り替えるように依頼するか (これは難しいかもしれません)、権限保護レベルを回避できるエクスプロイトを見つける (これも難しいかもしれません) ことで、システム キーでアプリケーションを署名させることができます。

さらに、この動作は、以下のものに関連しているように見えます。 問題 34792。Android Jelly Bean / 4.1: android.permission.READ_LOGS が動作しなくなった。 という、文書化されていない開発フラグと同じ保護レベルを使用する問題にも関連しているようです。

TelephonyManagerで作業することは良いように聞こえますが、実際にはそれほど簡単ではない適切なパーミッションを得なければ動作しません。

他の方法でTelephonyManagerを使うのはどうでしょうか?

悲しいかな、それはあなたが android.permission.MODIFY_PHONE_STATEを保持する必要があるようです。 を保持している必要があるようで、その結果、これらのメソッドにアクセスするのに苦労することになりそうです。


方法2:サービスコール SERVICE CODE

デバイス上で動作するビルドが指定されたコードで動作することをテストできる場合。

TelephonyManager と対話することができなくても、サービスとの対話のために service 実行ファイルを通してサービスと対話することもできます。

これはどのように動作するのですか?

かなり単純ですが、このルートに関するドキュメントは他のルートよりもさらに少ないです。実行ファイルが2つの引数 - サービス名とコード - を取ることは確かです。

  • サービス名 を使いたいのですが 電話 .

    これは、以下のように実行することで確認できます。 service list .

  • コード であったようです。 6 であったものが、現在は 5 .

    をベースにしているようです。 IBinder.FIRST_CALL_TRANSACTION + 5 から、多くのバージョンで ( 1.5_r4 から 4.4.4_r1 ) が、ローカルでのテストでは、コード 5 が着信に応答するように動作しました。ロリポは全体的に大規模なアップデートであるため、内部がここでも変更されたのは理解できることです。

この結果、コマンドの service call phone 5 .

プログラム的にどのように活用するか?

Java

以下のコードは、概念実証として機能するように作られた大まかな実装です。この方法を実際に使用したい場合は、次のサイトを参照してください。 問題なく su を使用するためのガイドライン をチェックして、おそらくより完全に開発された libsuperuser によって チェーンファイア .

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"/>

これって本当にroot権限が必要なんですか?

悲しいことに、そのようです。あなたは ランタイム.exec を使ってみることもできますが、私はそのルートでは運が悪かったです。

これはどの程度安定しているのでしょうか?

よくぞ聞いてくれました。ドキュメント化されていないため、上記のコードの違いに見られるように、これはさまざまなバージョンで壊れる可能性があります。サービス名はおそらく 電話 しかし、私たちが知っている限り、コードの値は同じバージョンの複数のビルド間で変わる可能性があり(たとえば、OEM のスキンによる内部修正)、その結果、使用した方法が壊れることがあります。したがって、テストはNexus 4 (mako/occam)で行われたことを述べておく価値があります。個人的にはこの方法を使わないことをお勧めしますが、より安定した方法を見つけることができないので、これが最善の方法だと信じています。


オリジナルの方法です。ヘッドセットのキーコードインテント

決済しなければならない時のために。

次のセクションは、強く影響を受けたものです。 この回答 によって ライリーC .

元の質問で投稿されたシミュレーションされたヘッドセット インテント方法は、期待どおりに放送されているようですが、呼び出しに応答するという目的を達成していないようです。これらの意図を処理するコードがあるように見えますが、単に気にされていないだけで、このメソッドに対して何らかの新しい対策があるはずです。ログにも興味深いものはなく、個人的には、Google が使用されている方法を簡単に壊すようなわずかな変更を導入する可能性があるため、このために Android のソースを調べることは価値がないだろうと思っています。

今すぐできることはありますか。

この動作は、入力実行ファイルを使って一貫して再現することができます。これはキーコードの引数を取り、その引数には単純に キーイベント.KEYCODE_HEADSETHOOK . このメソッドは root アクセスを必要としないので、一般的なユースケースに適していますが、このメソッドには小さな欠点があります。ヘッドセット ボタン押下イベントは許可を必要とするように指定できないため、実際のボタン押下のように動作してチェーン全体に波及していきます。

コード?

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();


tl;dr

Android 8.0 Oreo 以降では、素敵なパブリック API があります。

Android 8.0 Oreo より前のパブリック API はありません。内部 API は立ち入り禁止であるか、単にドキュメントがないだけです。注意して進める必要があります。