qnote Advent Calendar 2020 の9日目です。
今は昔、Android Lの頃はsetWifiApEnabled
をリフレクション で呼び出すことでテザリングの開始・停止を切り替えることができていました。
private fun changeTetheringState(state: Boolean) {
val wifi = applicationContext.getSystemService(WIFI_SERVICE) as WifiManager
val method = wifi.javaClass.getMethod("setWifiApEnabled", WifiConfiguration::class.java, Boolean::class.javaPrimitiveType)
method.invoke(wifi, null, state)
}
こんなやつですね
今更ですが、この方法がAndroid 8(APIレベル:26)からは使えなくなっています。
僕も最近になってやろうとしたら使えなくて困りました...。
元々がリフレクションを使ったやり方なので仕方ないですが、なんとかしたいですよね?
なんとかいたしましょう!!!
呼び出すべきメソッド
@SystemApi
@Deprecated
@RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED)
public void startTethering(int type, boolean showProvisioningUi, final OnStartTetheringCallback callback, Handler handler)
@SystemApi
@Deprecated
@RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED)
public void stopTethering(int type)
この2つのメソッドをリフレクションで呼び出します。
問題はstartTetheringの第三引数にあるOnStartTetheringCallbackで、これは抽象クラスとなっています。
@SystemApi
@Deprecated
public static abstract class OnStartTetheringCallback {
/**
* Called when tethering has been successfully started.
*/
public void onTetheringStarted() {}
/**
* Called when starting tethering failed.
*/
public void onTetheringFailed() {}
}
抽象クラスはリフレクションを使っても生成ができないので詰まってしまいました。
Dexmaker
隠された抽象クラスの生成について調べてみると以下の記事を見つけました。
java - Instance of abstract class with hidden constructor - Stack Overflow
こちらでDexmakerというライブラリを使って抽象クラスを生成できるという記述があったので試していきます。
ライブラリのページにはGradleのdependenciesに以下の記述をするようにあります。
androidTestImplementation ‘com.linkedin.dexmaker:dexmaker-mockito:2.28.0’
今回はテストで使う訳ではないのでimplementation
に書き換えてみたのですが、Stack OverflowにあるProxyBuilderなるクラスが存在しません…!
内容が古いのでダメなのか…と、半ば諦めていたのですが、
Stack Overflowでは「You will need dexmaker.1.4.jar and dexmaker-dx.1.4.jar」と書いてあったのでひとまずimplementation ‘com.linkedin.dexmaker:dexmaker:2.28.0’
に書き換えてみたところProxyBuilderが見つかりました!
わざわざ古い方でやる事もないと思うのでこのまま進めます。
実装
という訳で実装は以下のようになりました。
エラーハンドリング等々は特に考慮しませんでしたがとりあえず動いたコードになります。
private fun startTethering() {
fun getOnStartTetheringCallbackClass(): Class<*>? {
return Class.forName("android.net.ConnectivityManager\$OnStartTetheringCallback")
}
val connectivityManager = getSystemService(CONNECTIVITY_SERVICE) as ConnectivityManager
val proxy = ProxyBuilder.forClass(getOnStartTetheringCallbackClass())
.dexCache(cacheDir).handler(InvocationHandler { proxy, method, args -> ProxyBuilder.callSuper(proxy, method, *args) }).build()
val method: Method = connectivityManager.javaClass.getDeclaredMethod("startTethering", Int::class.javaPrimitiveType, Boolean::class.javaPrimitiveType, getOnStartTetheringCallbackClass(), Handler::class.java)
method.invoke(connectivityManager, ConnectivityManager.TYPE_MOBILE, false, proxy, null)
}
今回はテザリングできさえすればよかったのでコールバックは何もさせていませんが、もし「OnStartTetheringCallback」で何かしたいことがある場合には、「InvocationHandler」で「ProxyBuilder.callSuper」している箇所でmethod名で分岐させて処理を書けばOKです。
適当なコールバッククラスを作って事前に引数として渡すのも良さそうです。
「stopTethering」はもはやおまけですね…
private fun stopTethering() {
val connectivityManager = getSystemService(CONNECTIVITY_SERVICE) as ConnectivityManager
val method: Method = connectivityManager.javaClass.getDeclaredMethod("stopTethering", Int::class.javaPrimitiveType)
method.invoke(connectivityManager, ConnectivityManager.TYPE_MOBILE)
}
備考
テザリングのON/OFF切替にはシステム設定の変更の許可が必要で、普通のパーミッション許可と違うので、ストアリリースするアプリに組み込むのはちょっと抵抗があるかもしれません…
それから自分の端末ではないので詳細不明ですが一部端末で動作しないようです。
それから、今回使ったメソッドも@Deprecated
なのできっとまた使えなくなる日がくるのでしょう...