Oreo以降でもAndroidのWi-FiアクセスポイントをアプリでON/OFF切替できた！

[qnote Advent Calendar 2020](https://qiita.com/advent-calendar/2020/qnote) の９日目です。


今は昔、Android Lの頃は`setWifiApEnabled`をリフレクション で呼び出すことでテザリングの開始・停止を切り替えることができていました。


```sample.kt
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)からは使えなくなっています。
僕も最近になってやろうとしたら使えなくて困りました...。
元々がリフレクションを使ったやり方なので仕方ないですが、なんとかしたいですよね？

**なんとかいたしましょう！！！**

## 呼び出すべきメソッド
```startTethering.java
@SystemApi
@Deprecated
@RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED)
public void startTethering(int type, boolean showProvisioningUi, final OnStartTetheringCallback callback, Handler handler)
```

```stopTethering.java
@SystemApi
@Deprecated
@RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED)
public void stopTethering(int type)
```

この2つのメソッドをリフレクションで呼び出します。
問題は**startTethering**の第三引数にある**OnStartTetheringCallback**で、これは**抽象クラス**となっています。

```OnStartTetheringCallback.java
@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](https://stackoverflow.com/questions/29353382/instance-of-abstract-class-with-hidden-constructor)

こちらで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が見つかりました！**

わざわざ古い方でやる事もないと思うのでこのまま進めます。


## 実装
という訳で実装は以下のようになりました。
エラーハンドリング等々は特に考慮しませんでしたがとりあえず動いたコードになります。

```startTethering.kt
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」はもはやおまけですね…

```stopTethering.kt
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`なのできっとまた使えなくなる日がくるのでしょう...