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() { 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 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++ } } }