/* * Copyright © 2013 – 2015 Ricki Hirner (bitfire web engineering). * All rights reserved. This program and the accompanying materials * are made available under the terms of the GNU Public License v3.0 * which accompanies this distribution, and is available at * http://www.gnu.org/licenses/gpl.html */ package com.etesync.syncadapter.ui import android.Manifest import android.accounts.Account import android.accounts.AccountManager import android.annotation.SuppressLint import android.app.LoaderManager import android.content.* import android.content.pm.PackageManager import android.os.Build import android.os.Bundle import android.os.PowerManager import android.provider.CalendarContract import android.provider.ContactsContract import android.text.TextUtils import android.view.Menu import android.view.MenuItem import android.widget.TextView import androidx.core.content.ContextCompat import at.bitfire.ical4android.TaskProvider.ProviderName import at.bitfire.vcard4android.ContactsStorageException import com.etesync.syncadapter.* import com.etesync.syncadapter.Constants.KEY_ACCOUNT import com.etesync.journalmanager.Exceptions.HttpException import com.etesync.syncadapter.log.Logger import com.etesync.syncadapter.model.EntryEntity import com.etesync.syncadapter.model.JournalEntity import com.etesync.syncadapter.model.ServiceDB import com.etesync.syncadapter.model.ServiceEntity import com.etesync.syncadapter.resource.LocalAddressBook import org.acra.ACRA import org.apache.commons.lang3.exception.ExceptionUtils import org.apache.commons.lang3.text.WordUtils import java.io.File import java.util.logging.Level class DebugInfoActivity : BaseActivity(), LoaderManager.LoaderCallbacks { internal lateinit var tvReport: TextView internal lateinit var report: String internal var reportFile: File? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_debug_info) tvReport = findViewById(R.id.text_report) loaderManager.initLoader(0, intent.extras, this) } override fun onCreateOptionsMenu(menu: Menu): Boolean { menuInflater.inflate(R.menu.activity_debug_info, menu) return true } fun onShare(item: MenuItem) { ACRA.getErrorReporter().putCustomData("debug_info", report) ACRA.getErrorReporter().handleException(null) ACRA.getErrorReporter().removeCustomData("debug_info") } override fun onCreateLoader(id: Int, args: Bundle?): Loader { return ReportLoader(this, args) } override fun onLoadFinished(loader: Loader, data: String?) { if (data != null) { report = data tvReport.setText(report) } } override fun onLoaderReset(loader: Loader) {} internal class ReportLoader(context: Context, val extras: Bundle?) : AsyncTaskLoader(context) { override fun onStartLoading() { forceLoad() } @SuppressLint("MissingPermission") override fun loadInBackground(): String { var throwable: Throwable? = null var caller: String? = null var logs: String? = null var authority: String? = null var account: Account? = null var phase: String? = null if (extras != null) { throwable = extras.getSerializable(KEY_THROWABLE) as Throwable? caller = extras.getString(KEY_CALLER) logs = extras.getString(KEY_LOGS) account = extras.getParcelable(KEY_ACCOUNT) authority = extras.getString(KEY_AUTHORITY) phase = if (extras.containsKey(KEY_PHASE)) context.getString(extras.getInt(KEY_PHASE)) else null } val report = StringBuilder("--- BEGIN DEBUG INFO ---\n") // begin with most specific information if (phase != null) report.append("SYNCHRONIZATION INFO\nSynchronization phase: ").append(phase).append("\n") if (account != null) report.append("Account name: ").append(account.name).append("\n") if (authority != null) report.append("Authority: ").append(authority).append("\n") if (caller != null) report.append("Debug activity source: ").append(caller).append("\n") if (throwable is HttpException) { val http = throwable as HttpException? if (http!!.request != null) report.append("\nHTTP REQUEST:\n").append(http.request).append("\n\n") if (http.request != null) report.append("HTTP RESPONSE:\n").append(http.request).append("\n") } if (throwable != null) report.append("\nEXCEPTION:\n") .append(ExceptionUtils.getStackTrace(throwable)) if (logs != null) report.append("\nLOGS:\n").append(logs).append("\n") val context = context try { val pm = context.packageManager var installedFrom = pm.getInstallerPackageName(BuildConfig.APPLICATION_ID) if (TextUtils.isEmpty(installedFrom)) installedFrom = "APK (directly)" report.append("\nSOFTWARE INFORMATION\n" + "EteSync version: ").append(BuildConfig.VERSION_NAME).append(" (").append(BuildConfig.VERSION_CODE).append(") ").append("\n") .append("Installed from: ").append(installedFrom).append("\n") } catch (ex: Exception) { Logger.log.log(Level.SEVERE, "Couldn't get software information", ex) } report.append("CONFIGURATION\n") // power saving val powerManager = context.getSystemService(Context.POWER_SERVICE) as PowerManager? if (powerManager != null && Build.VERSION.SDK_INT >= 23) report.append("Power saving disabled: ") .append(if (powerManager.isIgnoringBatteryOptimizations(BuildConfig.APPLICATION_ID)) "yes" else "no") .append("\n") // permissions for (permission in arrayOf(Manifest.permission.READ_CONTACTS, Manifest.permission.WRITE_CONTACTS, Manifest.permission.READ_CALENDAR, Manifest.permission.WRITE_CALENDAR) + ProviderName.OpenTasks.permissions + ProviderName.TasksOrg.permissions) report.append(permission).append(" permission: ") .append(if (ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED) "granted" else "denied") .append("\n") // system-wide sync settings report.append("System-wide synchronization: ") .append(if (ContentResolver.getMasterSyncAutomatically()) "automatically" else "manually") .append("\n") // main accounts val accountManager = AccountManager.get(context) for (acct in accountManager.getAccountsByType(context.getString(R.string.account_type))) try { val settings = AccountSettings(context, acct) report.append("Account: ").append(acct.name).append("\n" + " Address book sync. interval: ").append(syncStatus(settings, App.addressBooksAuthority)).append("\n" + " Calendar sync. interval: ").append(syncStatus(settings, CalendarContract.AUTHORITY)).append("\n" + " OpenTasks sync. interval: ").append(syncStatus(settings, ProviderName.OpenTasks.authority)).append("\n" + " Tasks.org sync. interval: ").append(syncStatus(settings, ProviderName.TasksOrg.authority)).append("\n" + " WiFi only: ").append(settings.syncWifiOnly) if (settings.syncWifiOnlySSID != null) report.append(", SSID: ").append(settings.syncWifiOnlySSID) report.append("\n [CardDAV] Contact group method: ").append(settings.groupMethod) .append("\n Manage calendar colors: ").append(settings.manageCalendarColors) .append("\n") } catch (e: InvalidAccountException) { report.append(acct).append(" is invalid (unsupported settings version) or does not exist\n") } // address book accounts for (acct in accountManager.getAccountsByType(App.addressBookAccountType)) try { val addressBook = LocalAddressBook(context, acct, null) report.append("Address book account: ").append(acct.name).append("\n" + " Main account: ").append(addressBook.mainAccount).append("\n" + " URL: ").append(addressBook.url).append("\n" + " Sync automatically: ").append(ContentResolver.getSyncAutomatically(acct, ContactsContract.AUTHORITY)).append("\n") } catch (e: ContactsStorageException) { report.append(acct).append(" is invalid: ").append(e.message).append("\n") } report.append("\n") report.append("SQLITE DUMP\n") val dbHelper = ServiceDB.OpenHelper(context) dbHelper.dump(report) dbHelper.close() report.append("\n") report.append("SERVICES DUMP\n") val data = (getContext().applicationContext as App).data for (serviceEntity in data.select(ServiceEntity::class.java).get()) { report.append(serviceEntity.toString() + "\n") } report.append("\n") report.append("JOURNALS DUMP\n") val journals = data.select(JournalEntity::class.java).where(JournalEntity.DELETED.eq(false)).get().toList() for (journal in journals) { report.append(journal.toString() + "\n") val entryCount = data.count(EntryEntity::class.java).where(EntryEntity.JOURNAL.eq(journal)).get().value() report.append("\tEntries: " + entryCount.toString() + "\n\n") } report.append("\n") try { report.append( "SYSTEM INFORMATION\n" + "Android version: ").append(Build.VERSION.RELEASE).append(" (").append(Build.DISPLAY).append(")\n" + "Device: ").append(WordUtils.capitalize(Build.MANUFACTURER)).append(" ").append(Build.MODEL).append(" (").append(Build.DEVICE).append(")\n\n" ) } catch (ex: Exception) { Logger.log.log(Level.SEVERE, "Couldn't get system details", ex) } report.append("--- END DEBUG INFO ---\n") return report.toString() } protected fun syncStatus(settings: AccountSettings, authority: String): String { val interval = settings.getSyncInterval(authority) return if (interval != null) if (interval == AccountSettings.SYNC_INTERVAL_MANUALLY) "manually" else (interval / 60).toString() + " min" else "—" } } companion object { val KEY_CALLER = "caller" val KEY_THROWABLE = "throwable" val KEY_LOGS = "logs" val KEY_AUTHORITY = "authority" val KEY_PHASE = "phase" fun newIntent(context: Context?, caller: String): Intent { val intent = Intent(context, DebugInfoActivity::class.java) intent.putExtra(KEY_CALLER, caller) return intent } } }