Twidere-App-Android-Twitter.../twidere/src/main/kotlin/org/mariotaku/twidere/fragment/NetworkDiagnosticsFragment.kt

346 lines
14 KiB
Kotlin

package org.mariotaku.twidere.fragment
import android.accounts.AccountManager
import android.content.Context
import android.content.Intent
import android.net.ConnectivityManager
import android.net.Uri
import android.os.AsyncTask
import android.os.Bundle
import android.os.SystemClock
import androidx.core.content.ContextCompat
import android.text.Selection
import android.text.SpannableString
import android.text.Spanned
import android.text.method.ScrollingMovementMethod
import android.text.style.ForegroundColorSpan
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import kotlinx.android.synthetic.main.fragment_network_diagnostics.*
import okhttp3.Dns
import org.mariotaku.microblog.library.MicroBlog
import org.mariotaku.microblog.library.mastodon.Mastodon
import org.mariotaku.microblog.library.twitter.model.Paging
import org.mariotaku.restfu.RestFuUtils
import org.mariotaku.restfu.annotation.method.GET
import org.mariotaku.restfu.http.HttpRequest
import org.mariotaku.restfu.http.HttpResponse
import org.mariotaku.twidere.BuildConfig
import org.mariotaku.twidere.Constants.DEFAULT_TWITTER_API_URL_FORMAT
import org.mariotaku.twidere.R
import org.mariotaku.twidere.annotation.AccountType
import org.mariotaku.twidere.constant.SharedPreferenceConstants.*
import org.mariotaku.twidere.extension.model.getEndpoint
import org.mariotaku.twidere.extension.model.newMicroBlogInstance
import org.mariotaku.twidere.extension.restfu.headers
import org.mariotaku.twidere.extension.restfu.set
import org.mariotaku.twidere.model.account.cred.OAuthCredentials
import org.mariotaku.twidere.model.util.AccountUtils
import org.mariotaku.twidere.util.DataStoreUtils
import org.mariotaku.twidere.util.MicroBlogAPIFactory
import org.mariotaku.twidere.util.dagger.DependencyHolder
import org.mariotaku.twidere.util.net.SystemDnsFetcher
import org.mariotaku.twidere.util.net.TwidereDns
import java.io.IOException
import java.io.OutputStream
import java.lang.ref.WeakReference
import java.net.InetAddress
import java.util.*
/**
* Network diagnostics
* Created by mariotaku on 16/2/9.
*/
class NetworkDiagnosticsFragment : BaseFragment() {
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
startDiagnostics.setOnClickListener {
logText.text = null
DiagnosticsTask(this@NetworkDiagnosticsFragment).execute()
}
logText.movementMethod = ScrollingMovementMethod.getInstance()
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_network_diagnostics, container, false)
}
private fun appendMessage(message: LogText) {
val activity = activity ?: return
val coloredText = SpannableString.valueOf(message.message)
when (message.state) {
LogText.State.OK -> {
coloredText.setSpan(ForegroundColorSpan(ContextCompat.getColor(activity,
R.color.material_light_green)), 0, coloredText.length,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
}
LogText.State.ERROR -> {
coloredText.setSpan(ForegroundColorSpan(ContextCompat.getColor(activity,
R.color.material_red)), 0, coloredText.length,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
}
LogText.State.WARNING -> {
coloredText.setSpan(ForegroundColorSpan(ContextCompat.getColor(activity,
R.color.material_amber)), 0, coloredText.length,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
}
LogText.State.DEFAULT -> {
}
}
logText.append(coloredText)
Selection.setSelection(logText.editableText, logText.length())
}
internal class DiagnosticsTask(fragment: NetworkDiagnosticsFragment) : AsyncTask<Any, LogText, Unit>() {
private val fragmentRef = WeakReference(fragment)
private val contextRef = WeakReference(fragment.activity?.applicationContext)
override fun doInBackground(vararg params: Any) {
val context = contextRef.get() ?: return
val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
logPrintln("**** NOTICE ****", LogText.State.WARNING)
logPrintln()
logPrintln("Text below may have personal information, BE CAREFUL TO MAKE IT PUBLIC",
LogText.State.WARNING)
logPrintln()
val holder = DependencyHolder.get(context)
val dns = holder.dns
val prefs = holder.preferences
logPrintln(("Network preferences"))
logPrintln(("using_resolver: ${prefs.getBoolean(KEY_BUILTIN_DNS_RESOLVER, false)}"))
logPrintln(("tcp_dns_query: ${prefs.getBoolean(KEY_TCP_DNS_QUERY, false)}"))
logPrintln(("dns_server: ${prefs.getString(KEY_DNS_SERVER, null)}"))
logPrintln()
logPrintln(("System DNS servers"))
val servers = SystemDnsFetcher.get(context)
logPrintln(servers?.toString() ?: "null")
logPrintln()
for (accountKey in DataStoreUtils.getAccountKeys(context)) {
val details = AccountUtils.getAccountDetails(AccountManager.get(context),
accountKey, true) ?: continue
logPrintln(("Testing connection for account $accountKey"))
logPrintln()
logPrintln(("api_url_format: ${details.credentials.api_url_format}"))
(details.credentials as? OAuthCredentials)?.let { creds ->
logPrintln(("same_oauth_signing_url: ${creds.same_oauth_signing_url}"))
}
logPrintln(("auth_type: " + details.credentials_type))
logPrintln()
logPrintln(("Testing DNS functionality"))
logPrintln()
val endpoint = details.credentials.getEndpoint(MicroBlog::class.java)
val uri = Uri.parse(endpoint.url)
val host = uri.host
if (host != null) {
testDns(dns, host)
testNativeLookup(host)
} else {
logPrintln("API URL format is invalid", LogText.State.ERROR)
logPrintln()
}
logPrintln()
logPrintln(("Testing Network connectivity"))
logPrintln()
val baseUrl: String
baseUrl = if (details.credentials.api_url_format != null) {
MicroBlogAPIFactory.getApiBaseUrl(details.credentials.api_url_format, "api")
} else {
MicroBlogAPIFactory.getApiBaseUrl(DEFAULT_TWITTER_API_URL_FORMAT, "api")
}
val client = DependencyHolder.get(context).restHttpClient
var response: HttpResponse? = null
try {
logPrint("Connecting to $baseUrl...")
val builder = HttpRequest.Builder()
builder.method(GET.METHOD)
builder.url(baseUrl)
builder.headers {
this["Accept"] = "*/*"
}
val start = SystemClock.uptimeMillis()
response = client.newCall(builder.build()).execute()
logPrint(" OK (${SystemClock.uptimeMillis() - start} ms)")
} catch (e: IOException) {
logPrint(" ERROR: ${e.message}", LogText.State.ERROR)
}
logPrintln()
try {
if (response != null) {
logPrintln("Reading response...")
val start = SystemClock.uptimeMillis()
val os = CountOutputStream()
response.body.writeTo(os)
logPrintln(" ${os.total} bytes (${SystemClock.uptimeMillis() - start} ms)", LogText.State.OK)
}
} catch (e: IOException) {
logPrintln("ERROR: ${e.message}", LogText.State.ERROR)
} finally {
RestFuUtils.closeSilently(response)
}
logPrintln()
logPrintln(("Testing API functionality"))
logPrintln()
when (details.type) {
AccountType.MASTODON -> {
val mastodon = details.newMicroBlogInstance(context, Mastodon::class.java)
testAPICall("verify_credentials", mastodon) {
verifyCredentials()
}
testAPICall("get_home_timeline", mastodon) {
getHomeTimeline(Paging().count(1))
}
}
else -> {
val microBlog = details.newMicroBlogInstance(context, MicroBlog::class.java)
testAPICall("verify_credentials", microBlog) {
verifyCredentials()
}
testAPICall("get_home_timeline", microBlog) {
getHomeTimeline(Paging().count(1))
}
}
}
logPrintln()
}
logPrintln()
logPrintln(("Testing common host names"))
logPrintln()
testDns(dns, "www.google.com")
testNativeLookup("www.google.com")
logPrintln()
testDns(dns, "github.com")
testNativeLookup("github.com")
logPrintln()
testDns(dns, "twitter.com")
testNativeLookup("twitter.com")
logPrintln()
logPrintln("Build information: ")
logPrintln("version_code: ${BuildConfig.VERSION_CODE}")
logPrintln("version_name: ${BuildConfig.VERSION_NAME}")
logPrintln("flavor: ${BuildConfig.FLAVOR}")
logPrintln("debug: ${BuildConfig.DEBUG}")
logPrintln()
logPrintln(("Basic system information: "))
logPrintln(context.resources.configuration.toString())
logPrintln()
logPrintln(("Active network info: "))
logPrintln((cm.activeNetworkInfo.toString()))
}
override fun onProgressUpdate(vararg values: LogText) {
val fragment = fragmentRef.get() ?: return
for (value in values) {
fragment.appendMessage(value)
}
}
override fun onPreExecute() {
val fragment = fragmentRef.get() ?: return
fragment.diagStart()
super.onPreExecute()
}
override fun onPostExecute(u: Unit) {
val fragment = fragmentRef.get() ?: return
logPrintln()
logPrintln(("Done. You can send this log to me, and I'll contact you to solve related issue."))
fragment.logReady()
}
private fun testDns(dns: Dns, host: String) {
testCall("builtin lookup $host") {
if (dns is TwidereDns) {
logPrint((dns.lookupResolver(host).toString()))
} else {
logPrint((dns.lookup(host).toString()))
}
}
}
private fun testNativeLookup(host: String) {
testCall("native lookup $host") {
logPrint(Arrays.toString(InetAddress.getAllByName(host)))
}
}
private fun <T> testAPICall(name: String, api: T, test: T.() -> Unit) {
testCall(name) { test(api) }
}
private inline fun testCall(name: String, test: () -> Unit) {
logPrint("Testing $name...")
try {
val start = SystemClock.uptimeMillis()
test()
logPrint(" OK (${SystemClock.uptimeMillis() - start} ms)", LogText.State.OK)
} catch (e: Exception) {
logPrint(" ERROR: ${e.message}", LogText.State.ERROR)
}
logPrintln()
}
private fun logPrint(text: CharSequence, state: LogText.State = LogText.State.DEFAULT) {
publishProgress(LogText(text, state))
}
private fun logPrintln(text: CharSequence = "", state: LogText.State = LogText.State.DEFAULT) {
logPrint("$text\n", state)
}
}
private fun diagStart() {
startDiagnostics.setText(R.string.message_please_wait)
startDiagnostics.isEnabled = false
}
private fun logReady() {
startDiagnostics.setText(R.string.action_send)
startDiagnostics.isEnabled = true
startDiagnostics.setOnClickListener {
val intent = Intent(Intent.ACTION_SEND)
intent.type = "text/plain"
intent.putExtra(Intent.EXTRA_SUBJECT, "Twidere Network Diagnostics")
intent.putExtra(Intent.EXTRA_TEXT, logText.text)
startActivity(Intent.createChooser(intent, getString(R.string.action_send)))
}
}
internal data class LogText(val message: CharSequence, var state: State = State.DEFAULT) {
internal enum class State {
DEFAULT, OK, ERROR, WARNING
}
}
private class CountOutputStream : OutputStream() {
var total: Long = 0
private set
@Throws(IOException::class)
override fun write(oneByte: Int) {
total++
}
}
}