diff --git a/app/src/dummy/java/eu/faircode/email/DebugHelper.java b/app/src/dummy/java/eu/faircode/email/DebugHelper.java new file mode 100644 index 0000000000..03a4cdbbb3 --- /dev/null +++ b/app/src/dummy/java/eu/faircode/email/DebugHelper.java @@ -0,0 +1,38 @@ +package eu.faircode.email; + +/* + This file is part of FairEmail. + + FairEmail is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + FairEmail is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with FairEmail. If not, see . + + Copyright 2018-2023 by Marcel Bokhorst (M66B) +*/ + +import android.content.Context; +import android.os.Bundle; + +import org.json.JSONException; + +import java.io.IOException; + +public class DebugHelper { + static final String CRASH_LOG_NAME = "crash.log"; + + static EntityMessage getDebugInfo(Context context, String source, int title, Throwable ex, String log, Bundle args) throws IOException, JSONException { + return null; + } + + static void writeCrashLog(Context context, Throwable ex) { + } +} diff --git a/app/src/extra/java/eu/faircode/email/DebugHelper.java b/app/src/extra/java/eu/faircode/email/DebugHelper.java new file mode 100644 index 0000000000..9605d6666e --- /dev/null +++ b/app/src/extra/java/eu/faircode/email/DebugHelper.java @@ -0,0 +1,1942 @@ +package eu.faircode.email; + +/* + This file is part of FairEmail. + + FairEmail is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + FairEmail is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with FairEmail. If not, see . + + Copyright 2018-2023 by Marcel Bokhorst (M66B) +*/ + +import static androidx.browser.customtabs.CustomTabsService.ACTION_CUSTOM_TABS_CONNECTION; + +import android.Manifest; +import android.app.ActivityManager; +import android.app.ApplicationExitInfo; +import android.app.Notification; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.app.usage.UsageEvents; +import android.app.usage.UsageStatsManager; +import android.content.ComponentName; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.UriPermission; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.PermissionGroupInfo; +import android.content.pm.PermissionInfo; +import android.content.pm.ResolveInfo; +import android.content.pm.verify.domain.DomainVerificationManager; +import android.content.pm.verify.domain.DomainVerificationUserState; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.graphics.Point; +import android.net.ConnectivityManager; +import android.net.LinkProperties; +import android.net.Network; +import android.net.NetworkCapabilities; +import android.net.NetworkInfo; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.os.Debug; +import android.os.IBinder; +import android.os.LocaleList; +import android.os.PowerManager; +import android.os.SystemClock; +import android.os.ext.SdkExtensions; +import android.provider.MediaStore; +import android.provider.Settings; +import android.security.NetworkSecurityPolicy; +import android.text.TextUtils; +import android.view.Display; +import android.view.ViewConfiguration; +import android.view.WindowManager; + +import androidx.annotation.NonNull; +import androidx.annotation.RequiresApi; +import androidx.browser.customtabs.CustomTabsClient; +import androidx.browser.customtabs.CustomTabsServiceConnection; +import androidx.emoji2.text.EmojiCompat; +import androidx.preference.PreferenceManager; +import androidx.webkit.WebViewCompat; +import androidx.webkit.WebViewFeature; +import androidx.work.WorkInfo; +import androidx.work.WorkManager; +import androidx.work.WorkQuery; + +import net.openid.appauth.AuthState; +import net.openid.appauth.TokenResponse; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.FileWriter; +import java.io.IOException; +import java.io.OutputStream; +import java.net.InetAddress; +import java.net.InterfaceAddress; +import java.net.NetworkInterface; +import java.nio.charset.StandardCharsets; +import java.nio.file.FileStore; +import java.nio.file.FileSystems; +import java.security.KeyStore; +import java.security.Provider; +import java.security.Security; +import java.security.cert.X509Certificate; +import java.text.DateFormat; +import java.text.DateFormatSymbols; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.Date; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.Properties; +import java.util.Set; +import java.util.TimeZone; + +import javax.mail.Address; +import javax.mail.Part; +import javax.mail.internet.InternetAddress; +import javax.mail.internet.MimeUtility; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.X509TrustManager; + +public class DebugHelper { + static final String CRASH_LOG_NAME = "crash.log"; + + private static final long MAX_LOG_SIZE = 8 * 1024 * 1024L; + private static final long MIN_FILE_SIZE = 1024 * 1024L; + private static final long MIN_ZIP_SIZE = 2 * 1024 * 1024L; + + // https://docs.oracle.com/javase/8/docs/technotes/guides/net/proxies.html + // https://docs.oracle.com/javase/8/docs/api/java/net/doc-files/net-properties.html + private static final List NETWORK_PROPS = Collections.unmodifiableList(Arrays.asList( + "java.net.preferIPv4Stack", + "java.net.preferIPv6Addresses", + "http.proxyHost", + "http.proxyPort", + "http.nonProxyHosts", + "https.proxyHost", + "https.proxyPort", + //"ftp.proxyHost", + //"ftp.proxyPort", + //"ftp.nonProxyHosts", + "socksProxyHost", + "socksProxyPort", + "socksProxyVersion", + "java.net.socks.username", + //"java.net.socks.password", + "http.agent", + "http.keepalive", + "http.maxConnections", + "http.maxRedirects", + "http.auth.digest.validateServer", + "http.auth.digest.validateProxy", + "http.auth.digest.cnonceRepeat", + "http.auth.ntlm.domain", + "jdk.https.negotiate.cbt", + "networkaddress.cache.ttl", + "networkaddress.cache.negative.ttl" + )); + + static EntityMessage getDebugInfo(Context context, String source, int title, Throwable ex, String log, Bundle args) throws IOException, JSONException { + StringBuilder sb = new StringBuilder(); + sb.append(context.getString(title)).append("\n\n"); + if (args != null) { + sb.append(args.getString("issue")).append('\n'); + if (args.containsKey("account")) + sb.append('\n').append("Account: ").append(args.getString("account")); + if (args.containsKey("contact")) + sb.append('\n').append("Prior contact: ").append(args.getBoolean("contact")); + } + sb.append("\n\n"); + sb.append(getAppInfo(context)); + if (ex != null) { + ThrowableWrapper w = new ThrowableWrapper(ex); + sb.append(w.toSafeString()).append("\n").append(w.getSafeStackTraceString()); + } + if (log != null) + sb.append(log); + String body = "
" + TextUtils.htmlEncode(sb.toString()) + "
"; + + EntityMessage draft; + + DB db = DB.getInstance(context); + try { + db.beginTransaction(); + + List identities = db.identity().getComposableIdentities(null); + if (identities == null || identities.size() == 0) + throw new IllegalArgumentException(context.getString(R.string.title_no_composable)); + + EntityIdentity identity = identities.get(0); + EntityFolder drafts = db.folder().getFolderByType(identity.account, EntityFolder.DRAFTS); + if (drafts == null) + throw new IllegalArgumentException(context.getString(R.string.title_no_drafts)); + + draft = new EntityMessage(); + draft.account = drafts.account; + draft.folder = drafts.id; + draft.identity = identity.id; + draft.msgid = EntityMessage.generateMessageId(); + draft.thread = draft.msgid; + draft.from = new Address[]{new InternetAddress(identity.email, identity.name, StandardCharsets.UTF_8.name())}; + draft.to = new Address[]{Log.myAddress()}; + draft.subject = context.getString(R.string.app_name) + " " + getVersionInfo(context) + " debug info - " + source; + draft.received = new Date().getTime(); + draft.seen = true; + draft.ui_seen = true; + draft.id = db.message().insertMessage(draft); + + File file = draft.getFile(context); + Helper.writeText(file, body); // TODO CASA system info + db.message().setMessageContent(draft.id, true, null, 0, null, null); + + attachSettings(context, draft.id, 1); + attachAccounts(context, draft.id, 2); + attachNetworkInfo(context, draft.id, 3); + attachLog(context, draft.id, 4); + attachOperations(context, draft.id, 5); + attachTasks(context, draft.id, 6); + attachLogcat(context, draft.id, 7); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) + attachNotificationInfo(context, draft.id, 8); + attachEnvironment(context, draft.id, 9); + //if (MessageClassifier.isEnabled(context)) + // attachClassifierData(context, draft.id, 10); + + EntityOperation.queue(context, draft, EntityOperation.ADD); + + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + } + + ServiceSynchronize.eval(context, "debuginfo"); + + return draft; + } + + private static StringBuilder getAppInfo(Context context) { + StringBuilder sb = new StringBuilder(); + + ContentResolver resolver = context.getContentResolver(); + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + boolean main_log = prefs.getBoolean("main_log", true); + boolean protocol = prefs.getBoolean("protocol", false); + long last_cleanup = prefs.getLong("last_cleanup", 0); + + PackageManager pm = context.getPackageManager(); + + // Get version info + sb.append(String.format("%s %s\r\n", context.getString(R.string.app_name), getVersionInfo(context))); + sb.append(String.format("Package: %s uid: %d\r\n", + BuildConfig.APPLICATION_ID, android.os.Process.myUid())); + sb.append(String.format("Android: %s (SDK device=%d target=%d)\r\n", + Build.VERSION.RELEASE, Build.VERSION.SDK_INT, Helper.getTargetSdk(context))); + + String miui = Helper.getMIUIVersion(); + Integer autostart = (miui == null ? null : Helper.getMIUIAutostart(context)); + sb.append(String.format("MIUI: %s autostart: %s\r\n", + miui == null ? "-" : miui, + autostart == null ? "?" : Boolean.toString(autostart == 0))); + + boolean reporting = prefs.getBoolean("crash_reports", false); + if (reporting || BuildConfig.TEST_RELEASE) { + String uuid = prefs.getString("uuid", null); + sb.append(String.format("UUID: %s\r\n", uuid == null ? "-" : uuid)); + } + + try { + ApplicationInfo app = pm.getApplicationInfo(context.getPackageName(), PackageManager.GET_META_DATA); + String build_uuid = app.metaData.getString("com.bugsnag.android.BUILD_UUID"); + sb.append(String.format("Build UUID: %s\r\n", build_uuid == null ? "-" : build_uuid)); + } catch (PackageManager.NameNotFoundException ex) { + Log.e(ex); + } + + String installer = Helper.getInstallerName(context); + sb.append(String.format("Release: %s\r\n", Log.getReleaseType(context))); + sb.append(String.format("Play Store: %s\r\n", Helper.hasPlayStore(context))); + sb.append(String.format("Installer: %s\r\n", installer == null ? "-" : installer)); + sb.append(String.format("Installed: %s\r\n", new Date(Helper.getInstallTime(context)))); + sb.append(String.format("Updated: %s\r\n", new Date(Helper.getUpdateTime(context)))); + sb.append(String.format("Last cleanup: %s\r\n", new Date(last_cleanup))); + sb.append(String.format("Now: %s\r\n", new Date())); + sb.append(String.format("Zone: %s\r\n", TimeZone.getDefault().getID())); + + String language = prefs.getString("language", null); + sb.append(String.format("Locale: def=%s lang=%s\r\n", + Locale.getDefault(), language)); + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) + sb.append(String.format("System: %s\r\n", + Resources.getSystem().getConfiguration().locale)); + else { + LocaleList ll = Resources.getSystem().getConfiguration().getLocales(); + for (int i = 0; i < ll.size(); i++) + sb.append(String.format("System: %s\r\n", ll.get(i))); + } + + sb.append("\r\n"); + + String osVersion = null; + try { + osVersion = System.getProperty("os.version"); + } catch (Throwable ex) { + Log.e(ex); + } + + // Get device info + sb.append(String.format("Brand: %s\r\n", Build.BRAND)); + sb.append(String.format("Manufacturer: %s\r\n", Build.MANUFACTURER)); + sb.append(String.format("Model: %s\r\n", Build.MODEL)); + sb.append(String.format("Product: %s\r\n", Build.PRODUCT)); + sb.append(String.format("Device: %s\r\n", Build.DEVICE)); + sb.append(String.format("Host: %s\r\n", Build.HOST)); + sb.append(String.format("Time: %s\r\n", new Date(Build.TIME).toString())); + sb.append(String.format("Display: %s\r\n", Build.DISPLAY)); + sb.append(String.format("Id: %s\r\n", Build.ID)); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) + sb.append(String.format("SoC: %s/%s\r\n", Build.SOC_MANUFACTURER, Build.SOC_MODEL)); + sb.append(String.format("OS version: %s\r\n", osVersion)); + sb.append("\r\n"); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + try { + // https://developer.android.com/reference/android/app/ApplicationExitInfo + boolean exits = false; + long from = new Date().getTime() - 30 * 24 * 3600 * 1000L; + ActivityManager am = Helper.getSystemService(context, ActivityManager.class); + List infos = am.getHistoricalProcessExitReasons( + context.getPackageName(), 0, 100); + for (ApplicationExitInfo info : infos) + if (info.getTimestamp() > from && + info.getImportance() >= ActivityManager.RunningAppProcessInfo.IMPORTANCE_GONE) { + exits = true; + sb.append(String.format("%s: %s\r\n", + new Date(info.getTimestamp()), + Helper.getExitReason(info.getReason()))); + } + if (!exits) + sb.append("No crashes\r\n"); + sb.append("\r\n"); + } catch (Throwable ex) { + sb.append(ex).append("\r\n"); + } + } + + sb.append(String.format("Log main: %b protocol: %b build: %b test: %b\r\n", + main_log, protocol, BuildConfig.DEBUG, BuildConfig.TEST_RELEASE)); + + int[] contacts = ContactInfo.getStats(); + sb.append(String.format("Contact lookup: %d cached: %d\r\n", + contacts[0], contacts[1])); + + sb.append(String.format("Accessibility: %b\r\n", Helper.isAccessibilityEnabled(context))); + + String charset = MimeUtility.getDefaultJavaCharset(); + sb.append(String.format("Default charset: %s/%s\r\n", charset, MimeUtility.mimeCharset(charset))); + + String emoji; + try { + if (EmojiCompat.isConfigured()) { + int emojiState = EmojiCompat.get().getLoadState(); + switch (emojiState) { + case EmojiCompat.LOAD_STATE_LOADING: + emoji = "Loading"; + break; + case EmojiCompat.LOAD_STATE_SUCCEEDED: + emoji = "Loaded"; + break; + case EmojiCompat.LOAD_STATE_FAILED: + emoji = "Failed"; + break; + case EmojiCompat.LOAD_STATE_DEFAULT: + emoji = "Not loaded"; + break; + default: + emoji = "?" + emojiState; + } + } else + emoji = "Disabled"; + } catch (Throwable ex) { + Log.e(ex); + emoji = ex.toString(); + } + + sb.append("Emoji: ").append(emoji).append("\r\n"); + + sb.append("Transliterate: ") + .append(TextHelper.canTransliterate()) + .append("\r\n"); + + sb.append("Classifier: ") + .append(Helper.humanReadableByteCount(MessageClassifier.getSize(context))) + .append("\r\n"); + + sb.append("\r\n"); + + int cpus = Runtime.getRuntime().availableProcessors(); + sb.append(String.format("Processors: %d\r\n", cpus)); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + long running = SystemClock.uptimeMillis() - android.os.Process.getStartUptimeMillis(); + long cpu = android.os.Process.getElapsedCpuTime(); + int util = (int) (running == 0 ? 0 : 100 * cpu / running / cpus); + sb.append(String.format("Uptime: %s CPU: %s %d%%\r\n", + Helper.formatDuration(running), Helper.formatDuration(cpu), util)); + } + + Boolean largeHeap; + try { + ApplicationInfo info = pm.getApplicationInfo(context.getPackageName(), 0); + largeHeap = (info.flags & ApplicationInfo.FLAG_LARGE_HEAP) != 0; + } catch (Throwable ex) { + largeHeap = null; + } + + ActivityManager am = Helper.getSystemService(context, ActivityManager.class); + ActivityManager.MemoryInfo mi = new ActivityManager.MemoryInfo(); + am.getMemoryInfo(mi); + sb.append(String.format("Memory class: %d/%d Large: %s MB Total: %s Low: %b\r\n", + am.getMemoryClass(), am.getLargeMemoryClass(), + largeHeap == null ? "?" : Boolean.toString(largeHeap), + Helper.humanReadableByteCount(mi.totalMem), + am.isLowRamDevice())); + + long storage_available = Helper.getAvailableStorageSpace(); + long storage_total = Helper.getTotalStorageSpace(); + long storage_used = Helper.getSizeUsed(context.getFilesDir()); + sb.append(String.format("Storage space: %s/%s App: %s\r\n", + Helper.humanReadableByteCount(storage_total - storage_available), + Helper.humanReadableByteCount(storage_total), + Helper.humanReadableByteCount(storage_used))); + + long cache_used = Helper.getSizeUsed(context.getCacheDir()); + long cache_quota = Helper.getCacheQuota(context); + sb.append(String.format("Cache space: %s/%s\r\n", + Helper.humanReadableByteCount(cache_used), + Helper.humanReadableByteCount(cache_quota))); + + Runtime rt = Runtime.getRuntime(); + long hused = (rt.totalMemory() - rt.freeMemory()) / 1024L / 1024L; + long hmax = rt.maxMemory() / 1024L / 1024L; + long nheap = Debug.getNativeHeapAllocatedSize() / 1024L / 1024L; + long nsize = Debug.getNativeHeapSize() / 1024 / 1024L; + sb.append(String.format("Heap usage: %d/%d MiB native: %d/%d MiB\r\n", hused, hmax, nheap, nsize)); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + int ipc = IBinder.getSuggestedMaxIpcSizeBytes(); + sb.append(String.format("IPC max: %s\r\n", Helper.humanReadableByteCount(ipc))); + } + + sb.append("\r\n"); + + WindowManager wm = Helper.getSystemService(context, WindowManager.class); + Display display = wm.getDefaultDisplay(); + Point dim = new Point(); + display.getSize(dim); + float density = context.getResources().getDisplayMetrics().density; + sb.append(String.format("Density 1dp=%f\r\n", density)); + sb.append(String.format("Resolution: %.2f x %.2f dp\r\n", dim.x / density, dim.y / density)); + + Configuration config = context.getResources().getConfiguration(); + + String size; + if (config.isLayoutSizeAtLeast(Configuration.SCREENLAYOUT_SIZE_XLARGE)) + size = "XLarge"; + else if (config.isLayoutSizeAtLeast(Configuration.SCREENLAYOUT_SIZE_LARGE)) + size = "Large"; + else if (config.isLayoutSizeAtLeast(Configuration.SCREENLAYOUT_SIZE_NORMAL)) + size = "Medium"; + else if (config.isLayoutSizeAtLeast(Configuration.SCREENLAYOUT_SIZE_SMALL)) + size = "Small"; + else + size = "size=" + (config.screenLayout & Configuration.SCREENLAYOUT_SIZE_MASK); + + String orientation; + if (config.orientation == Configuration.ORIENTATION_LANDSCAPE) + orientation = "Landscape"; + else if (config.orientation == Configuration.ORIENTATION_PORTRAIT) + orientation = "Portrait"; + else + orientation = "orientation=" + config.orientation; + + sb.append(String.format("%s %s\r\n", size, orientation)); + + try { + float animation_scale = Settings.Global.getFloat(resolver, + Settings.Global.WINDOW_ANIMATION_SCALE, 0f); + sb.append(String.format("Animation scale: %f %s\r\n", animation_scale, + animation_scale == 1f ? "" : "!!!")); + } catch (Throwable ex) { + sb.append(ex).append("\r\n"); + } + + int uiMode = context.getResources().getConfiguration().uiMode; + sb.append(String.format("UI mode: 0x")) + .append(Integer.toHexString(uiMode)) + .append(" night=").append(Helper.isNight(context)) + .append("\r\n"); + + String uiType = Helper.getUiModeType(context); + sb.append(String.format("UI type: %s %s\r\n", uiType, + "normal".equals(uiType) ? "" : "!!!")); + + sb.append(String.format("Darken support: %b\r\n", + WebViewEx.isFeatureSupported(context, WebViewFeature.ALGORITHMIC_DARKENING))); + try { + PackageInfo pkg = WebViewCompat.getCurrentWebViewPackage(context); + sb.append(String.format("WebView %d/%s\r\n", + pkg == null ? -1 : pkg.versionCode, + pkg == null ? null : pkg.versionName)); + } catch (Throwable ex) { + sb.append(ex).append("\r\n"); + } + + sb.append("\r\n"); + + Boolean ignoring = Helper.isIgnoringOptimizations(context); + sb.append(String.format("Battery optimizations: %s %s\r\n", + ignoring == null ? null : Boolean.toString(!ignoring), + Boolean.FALSE.equals(ignoring) ? "!!!" : "")); + + PowerManager power = Helper.getSystemService(context, PowerManager.class); + boolean psaving = power.isPowerSaveMode(); + sb.append(String.format("Battery saving: %s %s\r\n", psaving, psaving ? "!!!" : "")); + + sb.append(String.format("Charging: %b; level: %d\r\n", + Helper.isCharging(context), Helper.getBatteryLevel(context))); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + // https://developer.android.com/reference/android/app/usage/UsageStatsManager + UsageStatsManager usm = Helper.getSystemService(context, UsageStatsManager.class); + int bucket = usm.getAppStandbyBucket(); + boolean inactive = usm.isAppInactive(BuildConfig.APPLICATION_ID); + sb.append(String.format("Standby bucket: %d-%b-%s %s\r\n", + bucket, inactive, Helper.getStandbyBucketName(bucket), + (bucket <= UsageStatsManager.STANDBY_BUCKET_ACTIVE && !inactive ? "" : "!!!"))); + } + + boolean canExact = AlarmManagerCompatEx.canScheduleExactAlarms(context); + boolean hasExact = AlarmManagerCompatEx.hasExactAlarms(context); + sb.append(String.format("ExactAlarms can=%b has=%b %s\r\n", canExact, hasExact, + canExact ? "" : "!!!")); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + boolean restricted = am.isBackgroundRestricted(); + sb.append(String.format("Background restricted: %b %s\r\n", restricted, + restricted ? "!!!" : "")); + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + boolean saving = ConnectionHelper.isDataSaving(context); + sb.append(String.format("Data saving: %b %s\r\n", saving, + saving ? "!!!" : "")); + } + + try { + int finish_activities = Settings.Global.getInt(resolver, + Settings.Global.ALWAYS_FINISH_ACTIVITIES, 0); + sb.append(String.format("Always finish: %d %s\r\n", finish_activities, + finish_activities == 0 ? "" : "!!!")); + } catch (Throwable ex) { + sb.append(ex).append("\r\n"); + } + + sb.append("\r\n"); + + return sb; + } + + private static void attachSettings(Context context, long id, int sequence) { + try { + DB db = DB.getInstance(context); + + EntityAttachment attachment = new EntityAttachment(); + attachment.message = id; + attachment.sequence = sequence; + attachment.name = "settings.txt"; + attachment.type = "text/plain"; + attachment.disposition = Part.ATTACHMENT; + attachment.size = null; + attachment.progress = 0; + attachment.id = db.attachment().insertAttachment(attachment); + + long size = 0; + File file = attachment.getFile(context); + try (OutputStream os = new BufferedOutputStream(new FileOutputStream(file))) { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + + Map settings = prefs.getAll(); + List keys = new ArrayList<>(settings.keySet()); + Collections.sort(keys); + for (String key : keys) { + Object value = settings.get(key); + if ("wipe_mnemonic".equals(key) && value != null) + value = "[redacted]"; + else if ("cloud_user".equals(key) && value != null) + value = "[redacted]"; + else if ("cloud_password".equals(key) && value != null) + value = "[redacted]"; + else if ("pin".equals(key) && value != null) + value = "[redacted]"; + else if (key != null && key.startsWith("oauth.")) + value = "[redacted]"; + else if (key != null && key.startsWith("graph.contacts.")) + value = "[redacted]"; + size += write(os, key + "=" + value + "\r\n"); + } + + size += write(os, "\r\n"); + + try { + List names = new ArrayList<>(); + + Properties props = System.getProperties(); + Enumeration pnames = props.propertyNames(); + while (pnames.hasMoreElements()) + names.add((String) pnames.nextElement()); + + Collections.sort(names); + for (String name : names) + size += write(os, name + "=" + props.getProperty(name) + "\r\n"); + } catch (Throwable ex) { + size += write(os, ex.getMessage() + "\r\n"); + } + } + + db.attachment().setDownloaded(attachment.id, size); + } catch (Throwable ex) { + Log.e(ex); + } + } + + private static void attachAccounts(Context context, long id, int sequence) { + try { + DB db = DB.getInstance(context); + + EntityAttachment attachment = new EntityAttachment(); + attachment.message = id; + attachment.sequence = sequence; + attachment.name = "accounts.txt"; + attachment.type = "text/plain"; + attachment.disposition = Part.ATTACHMENT; + attachment.size = null; + attachment.progress = 0; + attachment.id = db.attachment().insertAttachment(attachment); + + DateFormat dtf = Helper.getDateTimeInstance(context, SimpleDateFormat.SHORT, SimpleDateFormat.SHORT); + + long size = 0; + File file = attachment.getFile(context); + try (OutputStream os = new BufferedOutputStream(new FileOutputStream(file))) { + List accounts = db.account().getAccounts(); + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + boolean enabled = prefs.getBoolean("enabled", true); + int pollInterval = ServiceSynchronize.getPollInterval(context); + boolean metered = prefs.getBoolean("metered", true); + Boolean ignoring = Helper.isIgnoringOptimizations(context); + boolean canSchedule = AlarmManagerCompatEx.canScheduleExactAlarms(context); + boolean auto_optimize = prefs.getBoolean("auto_optimize", false); + boolean schedule = prefs.getBoolean("schedule", false); + + String ds = ConnectionHelper.getDataSaving(context); + boolean vpn = ConnectionHelper.vpnActive(context); + boolean ng = false; + try { + PackageManager pm = context.getPackageManager(); + pm.getPackageInfo("eu.faircode.netguard", 0); + ng = true; + } catch (Throwable ignored) { + } + + Integer bucket = null; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) + try { + UsageStatsManager usm = Helper.getSystemService(context, UsageStatsManager.class); + bucket = usm.getAppStandbyBucket(); + } catch (Throwable ignored) { + } + + Integer filter = null; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + NotificationManager nm = Helper.getSystemService(context, NotificationManager.class); + filter = nm.getCurrentInterruptionFilter(); + } + + StringBuilder filters = new StringBuilder(); + StringBuilder sorts = new StringBuilder(); + for (String key : prefs.getAll().keySet()) + if (key.startsWith("filter_")) { + Object value = prefs.getAll().get(key); + if (Boolean.TRUE.equals(value)) + filters.append(' ').append(key.substring(7)).append('=').append(value); + } else if (key.startsWith("sort_")) { + Object value = prefs.getAll().get(key); + sorts.append(' ').append(key).append('=').append(value); + } + + size += write(os, "enabled=" + enabled + (enabled ? "" : " !!!") + + " interval=" + pollInterval + "\r\n" + + "metered=" + metered + (metered ? "" : " !!!") + + " saving=" + ds + ("enabled".equals(ds) ? " !!!" : "") + + " vpn=" + vpn + (vpn ? " !!!" : "") + + " ng=" + ng + "\r\n" + + "optimizing=" + (ignoring == null ? null : !ignoring) + (Boolean.FALSE.equals(ignoring) ? " !!!" : "") + + " bucket=" + (bucket == null ? null : + Helper.getStandbyBucketName(bucket) + + (bucket > UsageStatsManager.STANDBY_BUCKET_ACTIVE ? " !!!" : "")) + + " canSchedule=" + canSchedule + (canSchedule ? "" : " !!!") + + " auto_optimize=" + auto_optimize + (auto_optimize ? " !!!" : "") + + " notifications=" + (filter == null ? null : + Helper.getInterruptionFilter(filter) + + (filter == NotificationManager.INTERRUPTION_FILTER_ALL ? "" : " !!!")) + "\r\n" + + "accounts=" + accounts.size() + + " folders=" + db.folder().countSync() + "/" + db.folder().countTotal() + + " messages=" + db.message().countTotal() + + " rules=" + db.rule().countTotal() + + " ops=" + db.operation().getOperationCount() + + " outbox=" + db.message().countOutbox() + "\r\n" + + "filter " + filters + " " + sorts + + "\r\n\r\n"); + + if (schedule) { + int minuteStart = prefs.getInt("schedule_start", 0); + int minuteEnd = prefs.getInt("schedule_end", 0); + int minuteStartWeekend = prefs.getInt("schedule_start_weekend", minuteStart); + int minuteEndWeekend = prefs.getInt("schedule_end_weekend", minuteEnd); + + size += write(os, String.format("schedule %s...%s weekend %s...%s\r\n", + CalendarHelper.formatHour(context, minuteStart), + CalendarHelper.formatHour(context, minuteEnd), + CalendarHelper.formatHour(context, minuteStartWeekend), + CalendarHelper.formatHour(context, minuteEndWeekend))); + + String[] daynames = new DateFormatSymbols().getWeekdays(); + for (int i = 0; i < 7; i++) { + boolean day = prefs.getBoolean("schedule_day" + i, true); + boolean weekend = CalendarHelper.isWeekend(context, i + 1); + size += write(os, String.format("schedule %s=%b %s\r\n", + daynames[i + 1], day, weekend ? "weekend" : "")); + } + + size += write(os, "\r\n"); + } + + for (EntityAccount account : accounts) + if (account.synchronize) + try { + String info = "pwd"; + if (account.auth_type == ServiceAuthenticator.AUTH_TYPE_OAUTH || + account.auth_type == ServiceAuthenticator.AUTH_TYPE_GRAPH) + info = getTokenInfo(account.password, account.auth_type); + size += write(os, String.format("%s %s\r\n", account.name, info)); + + List identities = db.identity().getSynchronizingIdentities(account.id); + for (EntityIdentity identity : identities) + if (identity.auth_type == ServiceAuthenticator.AUTH_TYPE_OAUTH || + identity.auth_type == ServiceAuthenticator.AUTH_TYPE_GRAPH) + size += write(os, String.format("- %s %s\r\n", + identity.name, getTokenInfo(identity.password, identity.auth_type))); + } catch (Throwable ex) { + size += write(os, ex.toString() + "\r\n"); + } + + size += write(os, "\r\n"); + + Map unified = new HashMap<>(); + for (EntityFolder folder : db.folder().getFoldersByType(EntityFolder.INBOX)) + unified.put(folder.id, folder); + for (EntityFolder folder : db.folder().getFoldersUnified(null, false)) + unified.put(folder.id, folder); + + for (Long fid : unified.keySet()) { + EntityFolder folder = unified.get(fid); + EntityAccount account = db.account().getAccount(folder.account); + size += write(os, String.format("%s/%s:%s sync=%b unified=%b\r\n", + (account == null ? null : account.name), + folder.name, folder.type, folder.synchronize, folder.unified)); + } + + size += write(os, "\r\n"); + + for (EntityAccount account : accounts) { + if (account.synchronize) { + int content = 0; + int messages = 0; + List folders = db.folder().getFoldersEx(account.id); + for (TupleFolderEx folder : folders) { + content += folder.content; + messages += folder.messages; + } + + int blocked = db.contact().countBlocked(account.id); + + boolean unmetered = false; + boolean ignore_schedule = false; + try { + if (account.conditions != null) { + JSONObject jconditions = new JSONObject(account.conditions); + unmetered = jconditions.optBoolean("unmetered"); + ignore_schedule = jconditions.optBoolean("ignore_schedule"); + } + } catch (Throwable ignored) { + } + + size += write(os, account.id + ":" + account.name + (account.primary ? "*" : "") + + " " + (account.protocol == EntityAccount.TYPE_IMAP ? "IMAP" : "POP") + + " [" + (account.provider == null ? "" : account.provider) + + ":" + ServiceAuthenticator.getAuthTypeName(account.auth_type) + "]" + + " " + account.host + ":" + account.port + "/" + + EmailService.getEncryptionName(account.encryption) + + (account.insecure ? " !!!" : "") + + " sync=" + account.synchronize + + " exempted=" + account.poll_exempted + (pollInterval > 0 && account.poll_exempted ? " !!!" : "") + + " poll=" + account.poll_interval + + " ondemand=" + account.ondemand + (account.ondemand ? " !!!" : "") + + " msgs=" + content + "/" + messages + " max=" + account.max_messages + + " blocked=" + blocked + (blocked == 0 ? "" : " !!!") + + " ops=" + db.operation().getOperationCount(account.id) + + " schedule=" + (!ignore_schedule) + (ignore_schedule ? " !!!" : "") + + " unmetered=" + unmetered + (unmetered ? " !!!" : "") + + " quota=" + (account.quota_usage == null ? "-" : Helper.humanReadableByteCount(account.quota_usage)) + + "/" + (account.quota_limit == null ? "-" : Helper.humanReadableByteCount(account.quota_limit)) + + " " + account.state + + (account.last_connected == null ? "" : " " + dtf.format(account.last_connected)) + + (account.error == null ? "" : "\r\n" + account.error) + + "\r\n"); + + if (folders.size() > 0) + Collections.sort(folders, folders.get(0).getComparator(context)); + for (TupleFolderEx folder : folders) + if (folder.synchronize || account.protocol == EntityAccount.TYPE_POP) { + int unseen = db.message().countUnseen(folder.id); + int hidden = db.message().countHidden(folder.id); + int notifying = db.message().countNotifying(folder.id); + size += write(os, "- " + folder.id + ":" + folder.name + " " + + folder.type + (folder.inherited_type == null ? "" : "/" + folder.inherited_type) + + (folder.unified ? " unified" : "") + + (folder.notify ? " notify" : "") + + (Boolean.TRUE.equals(folder.subscribed) ? " subscribed" : "") + + " poll=" + folder.poll + (folder.poll || EntityFolder.INBOX.equals(folder.type) ? "" : " !!! ") + + " factor=" + folder.poll_factor + + " days=" + getDays(folder.sync_days) + "/" + getDays(folder.keep_days) + + " msgs=" + folder.content + "/" + folder.messages + "/" + folder.total + + " ops=" + db.operation().getOperationCount(folder.id, null) + + " unseen=" + unseen + " hidden=" + hidden + " notifying=" + notifying + + " " + folder.state + + (folder.last_sync == null ? "" : " " + dtf.format(folder.last_sync)) + + "\r\n"); + } + + List swipes = db.account().getAccountSwipes(account.id); + if (swipes == null) + size += write(os, "<> swipes?\r\n"); + else + for (TupleAccountSwipes swipe : swipes) { + size += write(os, "> " + EntityMessage.getSwipeType(swipe.swipe_left) + " " + + swipe.left_name + ":" + swipe.left_type + "\r\n"); + size += write(os, "< " + EntityMessage.getSwipeType(swipe.swipe_right) + " " + + swipe.right_name + ":" + swipe.right_type + "\r\n"); + } + + size += write(os, "\r\n"); + } + } + + for (EntityAccount account : accounts) + if (account.synchronize) { + List identities = db.identity().getIdentities(account.id); + for (EntityIdentity identity : identities) + if (identity.synchronize) { + size += write(os, account.name + "/" + identity.name + (identity.primary ? "*" : "") + " " + + identity.display + " " + identity.email + + (identity.self ? "" : " !self") + + " [" + (identity.provider == null ? "" : identity.provider) + + ":" + ServiceAuthenticator.getAuthTypeName(identity.auth_type) + "]" + + (TextUtils.isEmpty(identity.sender_extra_regex) ? "" : " regex=" + identity.sender_extra_regex) + + (!identity.sender_extra ? "" : " edit" + + (identity.sender_extra_name ? "+name" : "-name") + + (identity.reply_extra_name ? "+copy" : "-copy")) + + " " + identity.host + ":" + identity.port + "/" + + EmailService.getEncryptionName(identity.encryption) + + (identity.insecure ? " !!!" : "") + + " ops=" + db.operation().getOperationCount(EntityOperation.SEND) + + " max=" + (identity.max_size == null ? "-" : Helper.humanReadableByteCount(identity.max_size)) + + " " + identity.state + + (identity.last_connected == null ? "" : " " + dtf.format(identity.last_connected)) + + (identity.error == null ? "" : "\r\n" + identity.error) + + "\r\n"); + } + } + + size += write(os, "\r\n"); + + for (EntityAccount account : accounts) { + int ops = db.operation().getOperationCount(account.id); + if (account.synchronize || ops > 0) + try { + JSONObject jaccount = account.toJSON(); + jaccount.put("state", account.state == null ? "null" : account.state); + jaccount.put("warning", account.warning); + jaccount.put("operations", ops); + jaccount.put("error", account.error); + jaccount.put("capabilities", account.capabilities); + + if (account.last_connected != null) + jaccount.put("last_connected", new Date(account.last_connected).toString()); + + jaccount.put("keep_alive_ok", account.keep_alive_ok); + jaccount.put("keep_alive_failed", account.keep_alive_failed); + jaccount.put("keep_alive_succeeded", account.keep_alive_succeeded); + + jaccount.remove("password"); + + size += write(os, "==========\r\n"); + size += write(os, jaccount.toString(2) + "\r\n"); + + List folders = db.folder().getFolders(account.id, false, false); + if (folders.size() > 0) + Collections.sort(folders, folders.get(0).getComparator(context)); + for (EntityFolder folder : folders) { + JSONObject jfolder = folder.toJSON(); + jfolder.put("inherited_type", folder.inherited_type); + jfolder.put("level", folder.level); + jfolder.put("total", folder.total); + jfolder.put("initialize", folder.initialize); + jfolder.put("subscribed", folder.subscribed); + jfolder.put("state", folder.state == null ? "null" : folder.state); + jfolder.put("sync_state", folder.sync_state == null ? "null" : folder.sync_state); + jfolder.put("poll_count", folder.poll_count); + jfolder.put("read_only", folder.read_only); + jfolder.put("selectable", folder.selectable); + jfolder.put("inferiors", folder.inferiors); + jfolder.put("auto_add", folder.auto_add); + jfolder.put("flags", folder.flags == null ? null : TextUtils.join(",", folder.flags)); + jfolder.put("keywords", folder.keywords == null ? null : TextUtils.join(",", folder.keywords)); + jfolder.put("tbc", Boolean.TRUE.equals(folder.tbc)); + jfolder.put("rename", folder.rename); + jfolder.put("tbd", Boolean.TRUE.equals(folder.tbd)); + jfolder.put("operations", db.operation().getOperationCount(folder.id, null)); + jfolder.put("error", folder.error); + if (folder.last_sync != null) + jfolder.put("last_sync", new Date(folder.last_sync).toString()); + if (folder.last_sync_count != null) + jfolder.put("last_sync_count", folder.last_sync_count); + size += write(os, jfolder.toString(2) + "\r\n"); + } + + List identities = db.identity().getIdentities(account.id); + for (EntityIdentity identity : identities) + try { + JSONObject jidentity = identity.toJSON(); + jidentity.remove("password"); + jidentity.remove("signature"); + size += write(os, "----------\r\n"); + size += write(os, jidentity.toString(2) + "\r\n"); + } catch (JSONException ex) { + size += write(os, ex.toString() + "\r\n"); + } + } catch (JSONException ex) { + size += write(os, ex.toString() + "\r\n"); + } + } + } + + db.attachment().setDownloaded(attachment.id, size); + } catch (Throwable ex) { + Log.e(ex); + } + } + + private static void attachNetworkInfo(Context context, long id, int sequence) { + try { + DB db = DB.getInstance(context); + + EntityAttachment attachment = new EntityAttachment(); + attachment.message = id; + attachment.sequence = sequence; + attachment.name = "network.txt"; + attachment.type = "text/plain"; + attachment.disposition = Part.ATTACHMENT; + attachment.size = null; + attachment.progress = 0; + attachment.id = db.attachment().insertAttachment(attachment); + + Boolean isValidated = null; + Boolean isCaptive = null; + + long size = 0; + File file = attachment.getFile(context); + try (OutputStream os = new BufferedOutputStream(new FileOutputStream(file))) { + ConnectivityManager cm = Helper.getSystemService(context, ConnectivityManager.class); + + NetworkInfo ani = cm.getActiveNetworkInfo(); + if (ani != null) + size += write(os, "Active network info=" + ani + + " connecting=" + ani.isConnectedOrConnecting() + + " connected=" + ani.isConnected() + + " available=" + ani.isAvailable() + + " state=" + ani.getState() + "/" + ani.getDetailedState() + + " metered=" + cm.isActiveNetworkMetered() + + " roaming=" + ani.isRoaming() + + " type=" + ani.getType() + "/" + ani.getTypeName() + + "\r\n\r\n"); + + Network active = ConnectionHelper.getActiveNetwork(context); + NetworkInfo a = (active == null ? null : cm.getNetworkInfo(active)); + NetworkCapabilities c = (active == null ? null : cm.getNetworkCapabilities(active)); + LinkProperties p = (active == null ? null : cm.getLinkProperties(active)); + boolean n = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M); + size += write(os, "Active network=" + active + " native=" + n + "\r\n"); + size += write(os, " info=" + a + + " connecting=" + (a == null ? null : a.isConnectedOrConnecting()) + + " connected=" + (a == null ? null : a.isConnected()) + + " available=" + (a == null ? null : a.isAvailable()) + + " state=" + (a == null ? null : a.getState() + "/" + a.getDetailedState()) + + " roaming=" + (a == null ? null : a.isRoaming()) + + " type=" + (a == null ? null : a.getType() + "/" + a.getTypeName()) + + "\r\n"); + size += write(os, " caps=" + c + "\r\n"); + size += write(os, " props=" + p + "\r\n\r\n"); + + for (Network network : cm.getAllNetworks()) { + size += write(os, (network.equals(active) ? "active=" : "network=") + network + "\r\n"); + + NetworkCapabilities caps = cm.getNetworkCapabilities(network); + size += write(os, " caps=" + caps + "\r\n"); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + if (isValidated == null) + isValidated = false; + if (caps != null && caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)) + isValidated = true; + + if (isCaptive == null) + isCaptive = false; + if (caps != null && caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_CAPTIVE_PORTAL)) + isCaptive = true; + } + + LinkProperties props = cm.getLinkProperties(network); + size += write(os, " props=" + props + "\r\n"); + + size += write(os, "\r\n"); + } + + try { + Enumeration interfaces = NetworkInterface.getNetworkInterfaces(); + while (interfaces != null && interfaces.hasMoreElements()) { + NetworkInterface ni = interfaces.nextElement(); + size += write(os, "Interface=" + ni + "\r\n"); + for (InterfaceAddress iaddr : ni.getInterfaceAddresses()) { + InetAddress addr = iaddr.getAddress(); + size += write(os, " addr=" + addr + + (addr.isLoopbackAddress() ? " loopback" : "") + + (addr.isSiteLocalAddress() ? " site local (LAN)" : "") + + (addr.isLinkLocalAddress() ? " link local (device)" : "") + + (addr.isAnyLocalAddress() ? " any local" : "") + + (addr.isMulticastAddress() ? " multicast" : "") + "\r\n"); + } + size += write(os, "\r\n"); + } + } catch (Throwable ex) { + size += write(os, ex.getMessage() + "\r\n"); + } + + ConnectionHelper.NetworkState state = ConnectionHelper.getNetworkState(context); + size += write(os, "Connected=" + state.isConnected() + "\r\n"); + size += write(os, "Suitable=" + state.isSuitable() + "\r\n"); + size += write(os, "Unmetered=" + state.isUnmetered() + "\r\n"); + size += write(os, "Roaming=" + state.isRoaming() + "\r\n"); + size += write(os, "\r\n"); + + boolean[] has46 = ConnectionHelper.has46(context); + + size += write(os, "Has IPv4=" + has46[0] + " IPv6=" + has46[1] + "\r\n"); + size += write(os, "VPN active=" + ConnectionHelper.vpnActive(context) + "\r\n"); + size += write(os, "Data saving=" + ConnectionHelper.isDataSaving(context) + "\r\n"); + size += write(os, "Airplane=" + ConnectionHelper.airplaneMode(context) + "\r\n"); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) + size += write(os, "Cleartext permitted= " + + NetworkSecurityPolicy.getInstance().isCleartextTrafficPermitted() + "\r\n"); + size += write(os, "\r\n"); + + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + int timeout = prefs.getInt("timeout", EmailService.DEFAULT_CONNECT_TIMEOUT); + boolean metered = prefs.getBoolean("metered", true); + int download = prefs.getInt("download", MessageHelper.DEFAULT_DOWNLOAD_SIZE); + boolean download_limited = prefs.getBoolean("download_limited", false); + boolean roaming = prefs.getBoolean("roaming", true); + boolean rlah = prefs.getBoolean("rlah", true); + boolean download_headers = prefs.getBoolean("download_headers", false); + boolean download_eml = prefs.getBoolean("download_eml", false); + boolean download_plain = prefs.getBoolean("download_plain", false); + boolean standalone_vpn = prefs.getBoolean("standalone_vpn", false); + boolean require_validated = prefs.getBoolean("require_validated", false); + boolean require_validated_captive = prefs.getBoolean("require_validated_captive", true); + boolean vpn_only = prefs.getBoolean("vpn_only", false); + boolean tcp_keep_alive = prefs.getBoolean("tcp_keep_alive", false); + boolean ssl_harden = prefs.getBoolean("ssl_harden", false); + boolean ssl_harden_strict = (ssl_harden && prefs.getBoolean("ssl_harden_strict", false)); + boolean cert_strict = prefs.getBoolean("cert_strict", true); + boolean open_safe = prefs.getBoolean("open_safe", false); + + size += write(os, "timeout=" + timeout + "s" + (timeout == EmailService.DEFAULT_CONNECT_TIMEOUT ? "" : " !!!") + "\r\n"); + size += write(os, "metered=" + metered + (metered ? "" : " !!!") + "\r\n"); + size += write(os, "download=" + Helper.humanReadableByteCount(download) + + " unmetered=" + download_limited + (download_limited ? " !!!" : "") + "\r\n"); + size += write(os, "roaming=" + roaming + (roaming ? "" : " !!!") + "\r\n"); + size += write(os, "rlah=" + rlah + (rlah ? "" : " !!!") + "\r\n"); + + size += write(os, "headers=" + download_headers + (download_headers ? " !!!" : "") + "\r\n"); + size += write(os, "eml=" + download_eml + (download_eml ? " !!!" : "") + "\r\n"); + size += write(os, "plain=" + download_plain + (download_plain ? " !!!" : "") + "\r\n"); + + size += write(os, "captive=" + (isCaptive == null ? "-" : Boolean.toString(isCaptive)) + "\r\n"); + size += write(os, "validation=" + require_validated + (require_validated ? " !!!" : "") + + " captive=" + require_validated_captive + (require_validated_captive ? "" : " !!!") + "\r\n"); + size += write(os, "validated=" + (isValidated == null ? "-" : Boolean.toString(isValidated)) + + (Boolean.FALSE.equals(isValidated) && + (Boolean.TRUE.equals(isCaptive) ? require_validated_captive : require_validated) ? " !!!" : "") + "\r\n"); + + size += write(os, "standalone_vpn=" + standalone_vpn + (standalone_vpn ? " !!!" : "") + "\r\n"); + size += write(os, "vpn_only=" + vpn_only + (vpn_only ? " !!!" : "") + "\r\n"); + + size += write(os, "tcp_keep_alive=" + tcp_keep_alive + (tcp_keep_alive ? " !!!" : "") + "\r\n"); + size += write(os, "ssl_harden=" + ssl_harden + (ssl_harden ? " !!!" : "") + "\r\n"); + size += write(os, "ssl_harden_strict=" + ssl_harden_strict + (ssl_harden_strict ? " !!!" : "") + "\r\n"); + size += write(os, "cert_strict=" + cert_strict + (cert_strict ? " !!!" : "") + "\r\n"); + size += write(os, "open_safe=" + open_safe + "\r\n"); + + size += write(os, "\r\n"); + size += write(os, Log.getCiphers().toString()); + + try { + String algo = TrustManagerFactory.getDefaultAlgorithm(); + TrustManagerFactory tmf = TrustManagerFactory.getInstance(algo); + tmf.init((KeyStore) null); + + TrustManager[] tms = tmf.getTrustManagers(); + if (tms != null) + for (TrustManager tm : tms) { + size += write(os, String.format("Trust manager: %s (%s)\n", + tm.getClass().getName(), algo)); + if (tm instanceof X509TrustManager) + for (X509Certificate cert : ((X509TrustManager) tm).getAcceptedIssuers()) + size += write(os, String.format("- %s\n", cert.getIssuerDN())); + } + } catch (Throwable ex) { + size += write(os, ex.getMessage() + "\r\n"); + } + } + + db.attachment().setDownloaded(attachment.id, size); + } catch (Throwable ex) { + Log.e(ex); + } + } + + private static void attachLog(Context context, long id, int sequence) { + try { + DB db = DB.getInstance(context); + + EntityAttachment attachment = new EntityAttachment(); + attachment.message = id; + attachment.sequence = sequence; + attachment.name = "log.txt"; + attachment.type = "text/plain"; + attachment.disposition = Part.ATTACHMENT; + attachment.size = null; + attachment.progress = 0; + attachment.id = db.attachment().insertAttachment(attachment); + + long size = 0; + File file = attachment.getFile(context); + try (OutputStream os = new BufferedOutputStream(new FileOutputStream(file))) { + long from = new Date().getTime() - 24 * 3600 * 1000L; + DateFormat TF = Helper.getTimeInstance(context); + + for (EntityLog entry : db.log().getLogs(from, null)) + if (entry.data != null && entry.data.contains("backoff=")) + size += write(os, String.format("%s %s\r\n", + TF.format(entry.time), + entry.data)); + + size += write(os, "\r\n"); + + for (EntityLog entry : db.log().getLogs(from, null)) { + size += write(os, String.format("%s [%d:%d:%d:%d:%d] %s\r\n", + TF.format(entry.time), + entry.type.ordinal(), + (entry.thread == null ? 0 : entry.thread), + (entry.account == null ? 0 : entry.account), + (entry.folder == null ? 0 : entry.folder), + (entry.message == null ? 0 : entry.message), + entry.data)); + if (size > MAX_LOG_SIZE) { + size += write(os, "\r\n"); + break; + } + } + } + + db.attachment().setDownloaded(attachment.id, size); + if (!BuildConfig.DEBUG && size > MIN_ZIP_SIZE) + attachment.zip(context); + } catch (Throwable ex) { + Log.e(ex); + } + } + + private static void attachOperations(Context context, long id, int sequence) { + try { + DB db = DB.getInstance(context); + + EntityAttachment attachment = new EntityAttachment(); + attachment.message = id; + attachment.sequence = sequence; + attachment.name = "operations.txt"; + attachment.type = "text/plain"; + attachment.disposition = Part.ATTACHMENT; + attachment.size = null; + attachment.progress = 0; + attachment.id = db.attachment().insertAttachment(attachment); + + long size = 0; + File file = attachment.getFile(context); + try (OutputStream os = new BufferedOutputStream(new FileOutputStream(file))) { + DateFormat TF = Helper.getTimeInstance(context); + + for (EntityOperation op : db.operation().getOperations()) { + EntityAccount account = (op.account == null ? null : db.account().getAccount(op.account)); + EntityFolder folder = (op.folder == null ? null : db.folder().getFolder(op.folder)); + size += write(os, String.format("%s %s/%s %d %s/%d %s %s %s\r\n", + TF.format(op.created), + account == null ? null : account.name, + folder == null ? null : folder.name, + op.message == null ? -1 : op.message, + op.name, + op.tries, + op.args, + op.state, + op.error)); + } + } + + db.attachment().setDownloaded(attachment.id, size); + } catch (Throwable ex) { + Log.e(ex); + } + } + + private static void attachTasks(Context context, long id, int sequence) { + try { + DB db = DB.getInstance(context); + + EntityAttachment attachment = new EntityAttachment(); + attachment.message = id; + attachment.sequence = sequence; + attachment.name = "tasks.txt"; + attachment.type = "text/plain"; + attachment.disposition = Part.ATTACHMENT; + attachment.size = null; + attachment.progress = 0; + attachment.id = db.attachment().insertAttachment(attachment); + + long size = 0; + File file = attachment.getFile(context); + try (OutputStream os = new BufferedOutputStream(new FileOutputStream(file))) { + for (SimpleTask task : SimpleTask.getList()) + size += write(os, String.format("%s\r\n", task.toString())); + size += write(os, "\r\n"); + for (TwoStateOwner owner : TwoStateOwner.getList()) + size += write(os, String.format("%s\r\n", owner.toString())); + } + + db.attachment().setDownloaded(attachment.id, size); + } catch (Throwable ex) { + Log.e(ex); + } + } + + private static void attachLogcat(Context context, long id, int sequence) { + try { + DB db = DB.getInstance(context); + + EntityAttachment attachment = new EntityAttachment(); + attachment.message = id; + attachment.sequence = sequence; + attachment.name = "logcat.txt"; + attachment.type = "text/plain"; + attachment.disposition = Part.ATTACHMENT; + attachment.size = null; + attachment.progress = 0; + attachment.id = db.attachment().insertAttachment(attachment); + + attachment.zip(context, TinyLogConfigurationLoader.getFiles(context)); +/* + // https://cheatsheetseries.owasp.org/cheatsheets/OS_Command_Injection_Defense_Cheat_Sheet.html#java + ProcessBuilder pb = new ProcessBuilder("/system/bin/logcat", + "-d", + "-v", "threadtime", + //"-t", "1000", + Log.TAG + ":I"); + Map env = pb.environment(); + env.clear(); + pb.directory(context.getFilesDir()); + + Process proc = null; + File file = attachment.getFile(context); + try (OutputStream os = new BufferedOutputStream(new FileOutputStream(file))) { + proc = pb.start(); + + long size = 0; + try (BufferedReader br = new BufferedReader(new InputStreamReader(proc.getInputStream()))) { + String line; + while ((line = br.readLine()) != null) + size += write(os, line + "\r\n"); + } + + db.attachment().setDownloaded(attachment.id, size); + if (!BuildConfig.DEBUG && size > MIN_ZIP_SIZE) + attachment.zip(context); + } finally { + if (proc != null) + proc.destroy(); + } +*/ + } catch (Throwable ex) { + Log.e(ex); + } + } + + @RequiresApi(api = Build.VERSION_CODES.O) + private static void attachNotificationInfo(Context context, long id, int sequence) { + try { + DB db = DB.getInstance(context); + + EntityAttachment attachment = new EntityAttachment(); + attachment.message = id; + attachment.sequence = sequence; + attachment.name = "notification.txt"; + attachment.type = "text/plain"; + attachment.disposition = Part.ATTACHMENT; + attachment.size = null; + attachment.progress = 0; + attachment.id = db.attachment().insertAttachment(attachment); + + long size = 0; + File file = attachment.getFile(context); + try (OutputStream os = new BufferedOutputStream(new FileOutputStream(file))) { + NotificationManager nm = Helper.getSystemService(context, NotificationManager.class); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + boolean permission = Helper.hasPermission(context, Manifest.permission.POST_NOTIFICATIONS); + boolean enabled = nm.areNotificationsEnabled(); + size += write(os, String.format("Permission=%b %s Enabled=%b %s\r\n", + permission, (permission ? "" : "!!!"), + enabled, (enabled ? "" : "!!!"))); + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + boolean paused = nm.areNotificationsPaused(); + size += write(os, String.format("Paused=%b %s\r\n", + paused, (paused ? "!!!" : ""))); + } + + int filter = nm.getCurrentInterruptionFilter(); + size += write(os, String.format("Interruption filter allow=%s %s\r\n\r\n", + Helper.getInterruptionFilter(filter), + (filter == NotificationManager.INTERRUPTION_FILTER_ALL ? "" : "!!!"))); + + size += write(os, String.format("InCall=%b DND=%b\r\n\r\n", + MediaPlayerHelper.isInCall(context), + MediaPlayerHelper.isDnd(context))); + + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + + StringBuilder options = new StringBuilder(); + for (String key : prefs.getAll().keySet()) + if (key.startsWith("notify_")) { + Object value = prefs.getAll().get(key); + boolean mark = false; + if ("notify_known".equals(key) && Boolean.TRUE.equals(value)) + mark = true; + if ("notify_background_only".equals(key) && Boolean.TRUE.equals(value)) + mark = true; + if ("notify_suppress_in_car".equals(key) && Boolean.TRUE.equals(value)) + mark = true; + options.append(' ').append(key).append('=') + .append(value) + .append(mark ? " !!!" : "") + .append("\r\n"); + } + + if (options.length() > 0) { + options.append("\r\n"); + size += write(os, options.toString()); + } + + for (NotificationChannel channel : nm.getNotificationChannels()) + try { + JSONObject jchannel = NotificationHelper.channelToJSON(channel); + size += write(os, jchannel.toString(2) + "\r\n\r\n"); + } catch (JSONException ex) { + size += write(os, ex + "\r\n"); + } + + size += write(os, + String.format("Importance none=%d; min=%d; low=%d; default=%d; high=%d; max=%d; unspecified=%d\r\n", + NotificationManager.IMPORTANCE_NONE, + NotificationManager.IMPORTANCE_MIN, + NotificationManager.IMPORTANCE_LOW, + NotificationManager.IMPORTANCE_DEFAULT, + NotificationManager.IMPORTANCE_HIGH, + NotificationManager.IMPORTANCE_MAX, + NotificationManager.IMPORTANCE_UNSPECIFIED)); + size += write(os, + String.format("Visibility private=%d; public=%d; secret=%d\r\n", + Notification.VISIBILITY_PRIVATE, + Notification.VISIBILITY_PUBLIC, + Notification.VISIBILITY_SECRET)); + size += write(os, String.format("Interruption filter\r\n")); + size += write(os, String.format("- All: no notifications are suppressed.\r\n")); + size += write(os, String.format("- Priority: all notifications are suppressed except those that match the priority criteria. Some audio streams are muted.\r\n")); + size += write(os, String.format("- None: all notifications are suppressed and all audio streams (except those used for phone calls) and vibrations are muted.\r\n")); + size += write(os, String.format("- Alarm: all notifications except those of category alarm are suppressed. Some audio streams are muted.\r\n")); + } + + db.attachment().setDownloaded(attachment.id, size); + } catch (Throwable ex) { + Log.e(ex); + } + } + + private static void attachEnvironment(Context context, long id, int sequence) { + try { + DB db = DB.getInstance(context); + + EntityAttachment attachment = new EntityAttachment(); + attachment.message = id; + attachment.sequence = sequence; + attachment.name = "environment.txt"; + attachment.type = "text/plain"; + attachment.disposition = Part.ATTACHMENT; + attachment.size = null; + attachment.progress = 0; + attachment.id = db.attachment().insertAttachment(attachment); + + long now = new Date().getTime(); + PackageManager pm = context.getPackageManager(); + + long size = 0; + File file = attachment.getFile(context); + try (OutputStream os = new BufferedOutputStream(new FileOutputStream(file))) { + size += write(os, String.format("Photo picker=%b\r\n", Helper.hasPhotoPicker())); + size += write(os, String.format("Double tap timeout=%d\r\n", ViewConfiguration.getDoubleTapTimeout())); + size += write(os, String.format("Long press timeout=%d\r\n", ViewConfiguration.getLongPressTimeout())); + + for (Class cls : new Class[]{ + ActivitySendSelf.class, + ActivitySearch.class, + ActivityAnswer.class, + ReceiverAutoStart.class}) + size += write(os, String.format("%s=%b\r\n", + cls.getSimpleName(), Helper.isComponentEnabled(context, cls))); + size += write(os, "\r\n"); + + try { + ApplicationInfo app = pm.getApplicationInfo(context.getPackageName(), PackageManager.GET_META_DATA); + List metas = Log.getExtras(app.metaData); + size += write(os, "Manifest metas=" + (metas == null ? null : metas.size()) + "\r\n"); + for (String meta : metas) + size += write(os, String.format("%s\r\n", meta)); + } catch (Throwable ex) { + size += write(os, String.format("%s\r\n", ex)); + } + size += write(os, "\r\n"); + + int flags = PackageManager.GET_RESOLVED_FILTER; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) + flags |= PackageManager.MATCH_ALL; + + try { + Intent home = new Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_HOME); + List homes = context.getPackageManager().queryIntentActivities(home, PackageManager.MATCH_DEFAULT_ONLY); + size += write(os, "Launchers=" + (homes == null ? null : homes.size()) + "\r\n"); + if (homes != null) + for (ResolveInfo ri : homes) + size += write(os, String.format("Launcher=%s\r\n", ri.activityInfo.packageName)); + + ResolveInfo rid = context.getPackageManager().resolveActivity(home, PackageManager.MATCH_DEFAULT_ONLY); + size += write(os, String.format("Default launcher=%s\r\n", (rid == null ? null : rid.activityInfo.packageName))); + } catch (Throwable ex) { + size += write(os, String.format("%s\r\n", ex)); + } + size += write(os, "\r\n"); + + try { + Intent intent = new Intent(Intent.ACTION_VIEW) + //.addCategory(Intent.CATEGORY_BROWSABLE) + .setData(Uri.parse("http://example.com/")); + ResolveInfo main = pm.resolveActivity(intent, 0); + + List ris = pm.queryIntentActivities(intent, flags); + size += write(os, "Browsers=" + (ris == null ? null : ris.size()) + "\r\n"); + if (ris != null) + for (ResolveInfo ri : ris) { + CharSequence label = pm.getApplicationLabel(ri.activityInfo.applicationInfo); + + Intent serviceIntent = new Intent(); + serviceIntent.setAction(ACTION_CUSTOM_TABS_CONNECTION); + serviceIntent.setPackage(ri.activityInfo.packageName); + boolean tabs = (pm.resolveService(serviceIntent, 0) != null); + + StringBuilder sb = new StringBuilder(); + sb.append("Browser=").append(ri.activityInfo.packageName); + if (Objects.equals(main == null ? null : main.activityInfo.packageName, ri.activityInfo.packageName)) + sb.append("*"); + sb.append(" (").append(label).append(")"); + sb.append(" tabs=").append(tabs); + sb.append(" view=").append(ri.filter.hasAction(Intent.ACTION_VIEW)); + sb.append(" browsable=").append(ri.filter.hasCategory(Intent.CATEGORY_BROWSABLE)); + sb.append(" authorities=").append(ri.filter.authoritiesIterator() != null); + sb.append(" schemes="); + + boolean first = true; + Iterator schemeIter = ri.filter.schemesIterator(); + while (schemeIter.hasNext()) { + String scheme = schemeIter.next(); + if (first) + first = false; + else + sb.append(','); + sb.append(scheme); + } + + if (tabs && BuildConfig.DEBUG) + try { + boolean bindable = context.bindService(serviceIntent, new CustomTabsServiceConnection() { + @Override + public void onCustomTabsServiceConnected(@NonNull final ComponentName component, final CustomTabsClient client) { + try { + context.unbindService(this); + } catch (Throwable ex) { + Log.e(ex); + } + } + + @Override + public void onServiceDisconnected(final ComponentName component) { + // Do nothing + } + }, 0); + sb.append(" bindable=").append(bindable); + } catch (Throwable ex) { + size += write(os, ex.toString() + "\r\n"); + } + + sb.append("\r\n"); + size += write(os, sb.toString()); + } + + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + String open_with_pkg = prefs.getString("open_with_pkg", null); + boolean open_with_tabs = prefs.getBoolean("open_with_tabs", true); + size += write(os, String.format("Selected: %s tabs=%b\r\n", + open_with_pkg, open_with_tabs)); + } catch (Throwable ex) { + size += write(os, String.format("%s\r\n", ex)); + } + size += write(os, "\r\n"); + + try { + Intent intent = new Intent(MediaStore.Audio.Media.RECORD_SOUND_ACTION); + ResolveInfo main = pm.resolveActivity(intent, 0); + List ris = pm.queryIntentActivities(intent, flags); + size += write(os, "Recorders=" + (ris == null ? null : ris.size()) + "\r\n"); + if (ris != null) + for (ResolveInfo ri : ris) { + CharSequence label = pm.getApplicationLabel(ri.activityInfo.applicationInfo); + + StringBuilder sb = new StringBuilder(); + sb.append("Recorder=").append(ri.activityInfo.packageName); + if (Objects.equals(main.activityInfo.packageName, ri.activityInfo.packageName)) + sb.append("*"); + sb.append(" (").append(label).append(")"); + + sb.append("\r\n"); + size += write(os, sb.toString()); + } + } catch (Throwable ex) { + size += write(os, String.format("%s\r\n", ex)); + } + size += write(os, "\r\n"); + + try { + List uperms = context.getContentResolver().getPersistedUriPermissions(); + size += write(os, "Persisted URIs=" + (uperms == null ? null : uperms.size()) + "\r\n"); + if (uperms != null) + for (UriPermission uperm : uperms) { + size += write(os, String.format("%s r=%b w=%b %s\r\n", + uperm.getUri().toString(), + uperm.isReadPermission(), + uperm.isWritePermission(), + new Date(uperm.getPersistedTime()))); + } + } catch (Throwable ex) { + size += write(os, String.format("%s\r\n", ex)); + } + size += write(os, "\r\n"); + + try { + PackageInfo pi = pm.getPackageInfo(BuildConfig.APPLICATION_ID, PackageManager.GET_PERMISSIONS); + for (int i = 0; i < pi.requestedPermissions.length; i++) + if (pi.requestedPermissions[i] != null && + pi.requestedPermissions[i].startsWith("android.permission.")) { + boolean granted = ((pi.requestedPermissionsFlags[i] & PackageInfo.REQUESTED_PERMISSION_GRANTED) != 0); + size += write(os, String.format("%s=%b\r\n", + pi.requestedPermissions[i].replace("android.permission.", ""), granted)); + } + } catch (Throwable ex) { + size += write(os, String.format("%s\r\n", ex)); + } + size += write(os, "\r\n"); + + for (String prop : NETWORK_PROPS) + size += write(os, prop + "=" + System.getProperty(prop) + "\r\n"); + size += write(os, "\r\n"); + + ApplicationInfo ai = context.getApplicationInfo(); + if (ai != null) + size += write(os, String.format("Source: %s\r\n public: %s\r\n", + ai.sourceDir, ai.publicSourceDir)); + size += write(os, String.format("Files: %s\r\n", context.getFilesDir())); + + size += write(os, String.format("Cache: %s\r\n external: %s\n", + context.getCacheDir(), context.getExternalCacheDir())); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) + size += write(os, String.format("Data: %s\r\n", context.getDataDir().getAbsolutePath())); + size += write(os, String.format("Database: %s\r\n", + context.getDatabasePath(DB.DB_NAME))); + + try (Cursor cursor = SQLiteDatabase.create(null).rawQuery( + "SELECT sqlite_version() AS sqlite_version", null)) { + if (cursor.moveToNext()) + size += write(os, String.format("sqlite: %s\r\n", cursor.getString(0))); + } + try { + TupleFtsStats stats = db.message().getFts(); + size += write(os, String.format("fts: %d/%d %s\r\n", stats.fts, stats.total, + Helper.humanReadableByteCount(Fts4DbHelper.size(context)))); + } catch (Throwable ex) { + size += write(os, String.format("%s\r\n", ex)); + } + + size += write(os, "\r\n"); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + try { + DomainVerificationManager dvm = Helper.getSystemService(context, DomainVerificationManager.class); + DomainVerificationUserState userState = dvm.getDomainVerificationUserState(context.getPackageName()); + Map hostToStateMap = userState.getHostToStateMap(); + for (String key : hostToStateMap.keySet()) { + Integer stateValue = hostToStateMap.get(key); + if (stateValue == DomainVerificationUserState.DOMAIN_STATE_VERIFIED) + size += write(os, String.format("Verified: %s\r\n", key)); + else if (stateValue == DomainVerificationUserState.DOMAIN_STATE_SELECTED) + size += write(os, String.format("selected: %s\r\n", key)); + else + size += write(os, String.format("Unverified: %s (%d)\r\n", key, + stateValue == null ? -1 : stateValue)); + } + } catch (Throwable ex) { + size += write(os, String.format("%s\r\n", ex)); + } + size += write(os, "\r\n"); + } + + try { + List works = WorkManager + .getInstance(context) + .getWorkInfos(WorkQuery.fromStates( + WorkInfo.State.ENQUEUED, + WorkInfo.State.BLOCKED, + WorkInfo.State.RUNNING)) + .get(); + for (WorkInfo work : works) { + size += write(os, String.format("Work: %s\r\n", + work.toString())); + } + } catch (Throwable ex) { + size += write(os, String.format("%s\r\n", ex)); + } + + size += write(os, "\r\n"); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) + try { + Map exts = SdkExtensions.getAllExtensionVersions(); + for (Integer ext : exts.keySet()) + size += write(os, String.format("Extension %d / %d\r\n", ext, exts.get(ext))); + if (exts.size() > 0) + size += write(os, "\r\n"); + + size += write(os, String.format("Max. pick images: %d\r\n\r\n", MediaStore.getPickImagesMaxLimit())); + } catch (Throwable ex) { + size += write(os, String.format("%s\r\n", ex)); + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + try { + for (FileStore store : FileSystems.getDefault().getFileStores()) + if (!store.isReadOnly() && + store.getUsableSpace() != 0 && + !"tmpfs".equals(store.type())) { + long total = store.getTotalSpace(); + long unalloc = store.getUnallocatedSpace(); + size += write(os, String.format("%s %s %s/%s\r\n", + store, + store.type(), + Helper.humanReadableByteCount(total - unalloc), + Helper.humanReadableByteCount(total))); + } + } catch (IOException ex) { + size += write(os, String.format("%s\r\n", ex)); + } + size += write(os, "\r\n"); + } + + List files = new ArrayList<>(); + try { + files.addAll(Helper.listFiles(context.getFilesDir(), MIN_FILE_SIZE)); + } catch (Throwable ex) { + size += write(os, String.format("%s\r\n", ex)); + } + try { + files.addAll(Helper.listFiles(context.getCacheDir(), MIN_FILE_SIZE)); + } catch (Throwable ex) { + size += write(os, String.format("%s\r\n", ex)); + } + + Collections.sort(files, new Comparator() { + @Override + public int compare(File f1, File f2) { + return -Long.compare(f1.length(), f2.length()); + } + }); + + for (int i = 0; i < Math.min(100, files.size()); i++) + size += write(os, String.format("%d %s %s\r\n", i + 1, + Helper.humanReadableByteCount(files.get(i).length()), + files.get(i).getAbsoluteFile())); + size += write(os, "\r\n"); + + size += write(os, String.format("Configuration: %s\r\n\r\n", + context.getResources().getConfiguration())); + + for (Provider p : Security.getProviders()) + size += write(os, String.format("%s\r\n", p)); + size += write(os, "\r\n"); + + String pgpPackage = PgpHelper.getPackageName(context); + boolean pgpInstalled = PgpHelper.isOpenKeychainInstalled(context); + size += write(os, String.format("%s=%b\r\n", pgpPackage, pgpInstalled)); + + if (pgpInstalled) + try { + PackageInfo pi = pm.getPackageInfo(pgpPackage, PackageManager.GET_PERMISSIONS); + for (int i = 0; i < pi.requestedPermissions.length; i++) { + boolean granted = ((pi.requestedPermissionsFlags[i] & PackageInfo.REQUESTED_PERMISSION_GRANTED) != 0); + size += write(os, String.format("- %s=%b\r\n", pi.requestedPermissions[i], granted)); + } + } catch (Throwable ex) { + size += write(os, String.format("%s\r\n", ex)); + } + + try { + int maxKeySize = javax.crypto.Cipher.getMaxAllowedKeyLength("AES"); + size += write(os, context.getString(R.string.title_advanced_aes_key_size, + Helper.humanReadableByteCount(maxKeySize, false))); + size += write(os, "\r\n"); + } catch (Throwable ex) { + size += write(os, String.format("%s\r\n", ex)); + } + size += write(os, "\r\n"); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + try { + Map stats = Debug.getRuntimeStats(); + for (String key : stats.keySet()) + size += write(os, String.format("%s=%s\r\n", key, stats.get(key))); + } catch (Throwable ex) { + size += write(os, String.format("%s\r\n", ex)); + } + size += write(os, "\r\n"); + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + try { + // https://developer.android.com/reference/android/app/ApplicationExitInfo + ActivityManager am = Helper.getSystemService(context, ActivityManager.class); + List infos = am.getHistoricalProcessExitReasons( + context.getPackageName(), 0, 100); + for (ApplicationExitInfo info : infos) + size += write(os, String.format("%s: %s %s/%s reason=%s status=%d importance=%d\r\n", + new Date(info.getTimestamp()), info.getDescription(), + Helper.humanReadableByteCount(info.getPss() * 1024L), + Helper.humanReadableByteCount(info.getRss() * 1024L), + Helper.getExitReason(info.getReason()), info.getStatus(), info.getImportance())); + } catch (Throwable ex) { + size += write(os, String.format("%s\r\n", ex)); + } + + size += write(os, "\r\n"); + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) + try { + UsageStatsManager usm = Helper.getSystemService(context, UsageStatsManager.class); + UsageEvents events = usm.queryEventsForSelf(now - 12 * 3600L, now); + UsageEvents.Event event = new UsageEvents.Event(); + while (events != null && events.hasNextEvent()) { + events.getNextEvent(event); + size += write(os, String.format("%s %s %s b=%d s=%d\r\n", + new Date(event.getTimeStamp()), + Helper.getEventType(event.getEventType()), + event.getClassName(), + event.getAppStandbyBucket(), + event.getShortcutId())); + } + } catch (Throwable ex) { + size += write(os, String.format("%s\r\n", ex)); + } + + try { + List groups = pm.getAllPermissionGroups(0); + groups.add(0, null); // Ungrouped + + for (PermissionGroupInfo group : groups) { + String name = (group == null ? null : group.name); + size += write(os, String.format("\r\n%s\r\n", name == null ? "Ungrouped" : name)); + size += write(os, "----------------------------------------\r\n"); + + try { + for (PermissionInfo permission : pm.queryPermissionsByGroup(name, 0)) + size += write(os, String.format("%s\r\n", permission.name)); + } catch (Throwable ex) { + size += write(os, String.format("%s\r\n", ex)); + } + } + } catch (Throwable ex) { + size += write(os, String.format("%s\r\n", ex)); + } + } + + db.attachment().setDownloaded(attachment.id, size); + } catch (Throwable ex) { + Log.e(ex); + } + } + + private static void attachClassifierData(Context context, long id, int sequence) throws IOException, JSONException { + DB db = DB.getInstance(context); + + EntityAttachment attachment = new EntityAttachment(); + attachment.message = id; + attachment.sequence = sequence; + attachment.name = "classifier.json"; + attachment.type = "application/json"; + attachment.disposition = Part.ATTACHMENT; + attachment.size = null; + attachment.progress = 0; + attachment.id = db.attachment().insertAttachment(attachment); + + MessageClassifier.save(context); + File source = MessageClassifier.getFile(context, false); + File target = attachment.getFile(context); + Helper.copy(source, target); + + db.attachment().setDownloaded(attachment.id, target.length()); + } + + private static String getVersionInfo(Context context) { + return String.format("%s%s/%d%s%s%s\r\n", + BuildConfig.VERSION_NAME, + BuildConfig.REVISION, + Helper.hasValidFingerprint(context) ? 1 : 3, + BuildConfig.PLAY_STORE_RELEASE ? "p" : "", + BuildConfig.DEBUG ? "d" : "", + ActivityBilling.isPro(context) ? "+" : "-"); + } + + private static String getDays(Integer days) { + if (days == null) + return "?"; + else + return (days == Integer.MAX_VALUE ? "∞" : Integer.toString(days)); + } + + static String getTokenInfo(String password, int auth_type) throws JSONException { + AuthState authState = AuthState.jsonDeserialize(password); + Long expiration = authState.getAccessTokenExpirationTime(); + TokenResponse t = authState.getLastTokenResponse(); + Set scopeSet = (t == null ? null : t.getScopeSet()); + String[] scopes = (scopeSet == null ? new String[0] : scopeSet.toArray(new String[0])); + return String.format("%s expire=%s need=%b %s", + ServiceAuthenticator.getAuthTypeName(auth_type), + (expiration == null ? null : new Date(expiration)), + authState.getNeedsTokenRefresh(), + TextUtils.join(",", scopes)); + } + + private static int write(OutputStream os, String text) throws IOException { + byte[] bytes = text.getBytes(); + os.write(bytes); // TODO CASA system info + return bytes.length; + } + + static void writeCrashLog(Context context, Throwable ex) { + File file = new File(context.getFilesDir(), CRASH_LOG_NAME); + Log.w("Writing exception to " + file); + + try (FileWriter out = new FileWriter(file, true)) { + out.write(BuildConfig.VERSION_NAME + BuildConfig.REVISION + " " + new Date() + "\r\n"); + ThrowableWrapper w = new ThrowableWrapper(ex); + out.write(w.toSafeString() + "\r\n"); + out.write(w.getSafeStackTraceString() + "\r\n"); + } catch (IOException e) { + Log.e(e); + } + } +} diff --git a/app/src/main/java/eu/faircode/email/ActivitySetup.java b/app/src/main/java/eu/faircode/email/ActivitySetup.java index 474eee3943..e3009a6923 100644 --- a/app/src/main/java/eu/faircode/email/ActivitySetup.java +++ b/app/src/main/java/eu/faircode/email/ActivitySetup.java @@ -499,11 +499,15 @@ public class ActivitySetup extends ActivityBase implements FragmentManager.OnBac @Override protected Long onExecute(Context context, Bundle args) throws IOException, JSONException { - return Log.getDebugInfo(context, "setup", R.string.title_debug_info_remark, null, null, args).id; + EntityMessage m = DebugHelper.getDebugInfo(context, + "setup", R.string.title_debug_info_remark, null, null, args); + return (m == null ? null : m.id); } @Override protected void onExecuted(Bundle args, Long id) { + if (id == null) + return; startActivity(new Intent(ActivitySetup.this, ActivityCompose.class) .putExtra("action", "edit") .putExtra("id", id)); diff --git a/app/src/main/java/eu/faircode/email/ActivityView.java b/app/src/main/java/eu/faircode/email/ActivityView.java index 574d36c348..3eff57835c 100644 --- a/app/src/main/java/eu/faircode/email/ActivityView.java +++ b/app/src/main/java/eu/faircode/email/ActivityView.java @@ -1502,7 +1502,7 @@ public class ActivityView extends ActivityBilling implements FragmentManager.OnB new SimpleTask() { @Override protected Long onExecute(Context context, Bundle args) throws Throwable { - File file = new File(context.getFilesDir(), Log.CRASH_LOG_NAME); + File file = new File(context.getFilesDir(), DebugHelper.CRASH_LOG_NAME); if (file.exists()) { StringBuilder sb = new StringBuilder(); try { @@ -1512,7 +1512,9 @@ public class ActivityView extends ActivityBilling implements FragmentManager.OnB sb.append(line).append("\r\n"); } - return Log.getDebugInfo(context, "crash", R.string.title_crash_info_remark, null, sb.toString(), null).id; + EntityMessage m = DebugHelper.getDebugInfo(context, + "crash", R.string.title_crash_info_remark, null, sb.toString(), null); + return (m == null ? null : m.id); } finally { Helper.secureDelete(file); } @@ -1523,11 +1525,12 @@ public class ActivityView extends ActivityBilling implements FragmentManager.OnB @Override protected void onExecuted(Bundle args, Long id) { - if (id != null) - startActivity( - new Intent(ActivityView.this, ActivityCompose.class) - .putExtra("action", "edit") - .putExtra("id", id)); + if (id == null) + return; + startActivity( + new Intent(ActivityView.this, ActivityCompose.class) + .putExtra("action", "edit") + .putExtra("id", id)); } @Override @@ -1535,7 +1538,7 @@ public class ActivityView extends ActivityBilling implements FragmentManager.OnB ToastEx.makeText(ActivityView.this, Log.formatThrowable(ex, false), Toast.LENGTH_LONG).show(); } - }.execute(this, new Bundle(), Log.CRASH_LOG_NAME); + }.execute(this, new Bundle(), DebugHelper.CRASH_LOG_NAME); } private void checkUpdate(boolean always) { @@ -2277,14 +2280,17 @@ public class ActivityView extends ActivityBilling implements FragmentManager.OnB protected Long onExecute(Context context, Bundle args) throws IOException, JSONException { boolean send = args.getBoolean("send"); - long id = Log.getDebugInfo(context, "main", R.string.title_debug_info_remark, null, null, args).id; + EntityMessage m = DebugHelper.getDebugInfo(context, + "main", R.string.title_debug_info_remark, null, null, args); + if (m == null) + return null; if (send) { DB db = DB.getInstance(context); try { db.beginTransaction(); - EntityMessage draft = db.message().getMessage(id); + EntityMessage draft = db.message().getMessage(m.id); if (draft != null) { draft.folder = EntityFolder.getOutbox(context).id; db.message().updateMessage(draft); @@ -2301,11 +2307,14 @@ public class ActivityView extends ActivityBilling implements FragmentManager.OnB } } - return id; + return m.id; } @Override protected void onExecuted(Bundle args, Long id) { + if (id == null) + return; + boolean sent = args.getBoolean("sent"); if (sent) { ToastEx.makeText(ActivityView.this, R.string.title_debug_info_send, Toast.LENGTH_LONG).show(); diff --git a/app/src/main/java/eu/faircode/email/ApplicationEx.java b/app/src/main/java/eu/faircode/email/ApplicationEx.java index d22acaf51d..2457e20777 100644 --- a/app/src/main/java/eu/faircode/email/ApplicationEx.java +++ b/app/src/main/java/eu/faircode/email/ApplicationEx.java @@ -151,7 +151,7 @@ public class ApplicationEx extends Application if (BuildConfig.BETA_RELEASE || !Helper.isPlayStoreInstall()) - Log.writeCrashLog(ApplicationEx.this, ex); + DebugHelper.writeCrashLog(ApplicationEx.this, ex); if (prev != null) prev.uncaughtException(thread, ex); diff --git a/app/src/main/java/eu/faircode/email/Log.java b/app/src/main/java/eu/faircode/email/Log.java index 0efd347c56..0a58a6c477 100644 --- a/app/src/main/java/eu/faircode/email/Log.java +++ b/app/src/main/java/eu/faircode/email/Log.java @@ -19,61 +19,23 @@ package eu.faircode.email; Copyright 2018-2023 by Marcel Bokhorst (M66B) */ -import static androidx.browser.customtabs.CustomTabsService.ACTION_CUSTOM_TABS_CONNECTION; - -import android.Manifest; import android.app.ActivityManager; -import android.app.ApplicationExitInfo; import android.app.Dialog; -import android.app.Notification; -import android.app.NotificationChannel; -import android.app.NotificationManager; -import android.app.usage.UsageEvents; -import android.app.usage.UsageStatsManager; -import android.content.ComponentName; -import android.content.ContentResolver; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; -import android.content.UriPermission; -import android.content.pm.ApplicationInfo; -import android.content.pm.PackageInfo; -import android.content.pm.PackageManager; -import android.content.pm.PermissionGroupInfo; -import android.content.pm.PermissionInfo; -import android.content.pm.ResolveInfo; -import android.content.pm.verify.domain.DomainVerificationManager; -import android.content.pm.verify.domain.DomainVerificationUserState; -import android.content.res.Configuration; -import android.content.res.Resources; -import android.database.Cursor; -import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteFullException; -import android.graphics.Point; import android.graphics.Typeface; -import android.net.ConnectivityManager; -import android.net.LinkProperties; -import android.net.Network; -import android.net.NetworkCapabilities; -import android.net.NetworkInfo; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.os.DeadObjectException; import android.os.DeadSystemException; import android.os.Debug; -import android.os.IBinder; -import android.os.LocaleList; import android.os.OperationCanceledException; -import android.os.PowerManager; import android.os.RemoteException; -import android.os.SystemClock; import android.os.TransactionTooLargeException; -import android.os.ext.SdkExtensions; -import android.provider.MediaStore; -import android.provider.Settings; -import android.security.NetworkSecurityPolicy; import android.text.SpannableStringBuilder; import android.text.Spanned; import android.text.TextUtils; @@ -81,30 +43,18 @@ import android.text.style.RelativeSizeSpan; import android.text.style.StrikethroughSpan; import android.text.style.StyleSpan; import android.util.Printer; -import android.view.Display; import android.view.InflateException; import android.view.LayoutInflater; import android.view.View; -import android.view.ViewConfiguration; -import android.view.WindowManager; import android.widget.Button; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.annotation.RequiresApi; import androidx.appcompat.app.AlertDialog; -import androidx.browser.customtabs.CustomTabsClient; -import androidx.browser.customtabs.CustomTabsServiceConnection; -import androidx.emoji2.text.EmojiCompat; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; import androidx.preference.PreferenceManager; -import androidx.webkit.WebViewCompat; -import androidx.webkit.WebViewFeature; -import androidx.work.WorkInfo; -import androidx.work.WorkManager; -import androidx.work.WorkQuery; import com.bugsnag.android.BreadcrumbType; import com.bugsnag.android.Bugsnag; @@ -121,119 +71,51 @@ import com.sun.mail.iap.ProtocolException; import com.sun.mail.util.FolderClosedIOException; import com.sun.mail.util.MailConnectException; -import net.openid.appauth.AuthState; -import net.openid.appauth.TokenResponse; - import org.bouncycastle.jsse.provider.BouncyCastleJsseProvider; -import org.json.JSONException; -import org.json.JSONObject; -import java.io.BufferedOutputStream; -import java.io.BufferedReader; -import java.io.File; import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.FileWriter; import java.io.IOException; -import java.io.InputStreamReader; -import java.io.OutputStream; import java.io.UnsupportedEncodingException; import java.lang.reflect.Array; -import java.net.InetAddress; -import java.net.InterfaceAddress; -import java.net.NetworkInterface; import java.net.SocketException; import java.net.SocketTimeoutException; import java.net.UnknownHostException; import java.nio.charset.StandardCharsets; -import java.nio.file.FileStore; -import java.nio.file.FileSystems; import java.security.KeyStore; import java.security.Provider; -import java.security.Security; import java.security.cert.CertPathValidatorException; -import java.security.cert.X509Certificate; -import java.text.DateFormat; -import java.text.DateFormatSymbols; -import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; -import java.util.Comparator; import java.util.Date; -import java.util.Enumeration; import java.util.HashMap; import java.util.HashSet; -import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.Map; -import java.util.Objects; -import java.util.Properties; import java.util.Set; -import java.util.TimeZone; import java.util.UUID; import java.util.concurrent.TimeoutException; -import javax.mail.Address; import javax.mail.AuthenticationFailedException; import javax.mail.FolderClosedException; import javax.mail.MessageRemovedException; import javax.mail.MessagingException; -import javax.mail.Part; import javax.mail.StoreClosedException; import javax.mail.internet.InternetAddress; -import javax.mail.internet.MimeUtility; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLHandshakeException; import javax.net.ssl.SSLPeerUnverifiedException; import javax.net.ssl.SSLSocket; import javax.net.ssl.TrustManager; import javax.net.ssl.TrustManagerFactory; -import javax.net.ssl.X509TrustManager; public class Log { private static Context ctx; - static final String CRASH_LOG_NAME = "crash.log"; - - private static final long MAX_LOG_SIZE = 8 * 1024 * 1024L; private static final int MAX_CRASH_REPORTS = (BuildConfig.TEST_RELEASE ? 50 : 5); - private static final long MIN_FILE_SIZE = 1024 * 1024L; - private static final long MIN_ZIP_SIZE = 2 * 1024 * 1024L; private static final String TAG = "fairemail"; - // https://docs.oracle.com/javase/8/docs/technotes/guides/net/proxies.html - // https://docs.oracle.com/javase/8/docs/api/java/net/doc-files/net-properties.html - private static final List NETWORK_PROPS = Collections.unmodifiableList(Arrays.asList( - "java.net.preferIPv4Stack", - "java.net.preferIPv6Addresses", - "http.proxyHost", - "http.proxyPort", - "http.nonProxyHosts", - "https.proxyHost", - "https.proxyPort", - //"ftp.proxyHost", - //"ftp.proxyPort", - //"ftp.nonProxyHosts", - "socksProxyHost", - "socksProxyPort", - "socksProxyVersion", - "java.net.socks.username", - //"java.net.socks.password", - "http.agent", - "http.keepalive", - "http.maxConnections", - "http.maxRedirects", - "http.auth.digest.validateServer", - "http.auth.digest.validateProxy", - "http.auth.digest.cnonceRepeat", - "http.auth.ntlm.domain", - "jdk.https.negotiate.cbt", - "networkaddress.cache.ttl", - "networkaddress.cache.negative.ttl" - )); - static final String TOKEN_REFRESH_REQUIRED = "Token refresh required. Is there a VPN based app running?"; @@ -1845,98 +1727,6 @@ public class Log { return sb.toString(); } - static void writeCrashLog(Context context, Throwable ex) { - File file = new File(context.getFilesDir(), CRASH_LOG_NAME); - Log.w("Writing exception to " + file); - - try (FileWriter out = new FileWriter(file, true)) { - out.write(BuildConfig.VERSION_NAME + BuildConfig.REVISION + " " + new Date() + "\r\n"); - ThrowableWrapper w = new ThrowableWrapper(ex); - out.write(w.toSafeString() + "\r\n"); - out.write(w.getSafeStackTraceString() + "\r\n"); - } catch (IOException e) { - Log.e(e); - } - } - - static EntityMessage getDebugInfo(Context context, String source, int title, Throwable ex, String log, Bundle args) throws IOException, JSONException { - StringBuilder sb = new StringBuilder(); - sb.append(context.getString(title)).append("\n\n"); - if (args != null) { - sb.append(args.getString("issue")).append('\n'); - if (args.containsKey("account")) - sb.append('\n').append("Account: ").append(args.getString("account")); - if (args.containsKey("contact")) - sb.append('\n').append("Prior contact: ").append(args.getBoolean("contact")); - } - sb.append("\n\n"); - sb.append(getAppInfo(context)); - if (ex != null) { - ThrowableWrapper w = new ThrowableWrapper(ex); - sb.append(w.toSafeString()).append("\n").append(w.getSafeStackTraceString()); - } - if (log != null) - sb.append(log); - String body = "
" + TextUtils.htmlEncode(sb.toString()) + "
"; - - EntityMessage draft; - - DB db = DB.getInstance(context); - try { - db.beginTransaction(); - - List identities = db.identity().getComposableIdentities(null); - if (identities == null || identities.size() == 0) - throw new IllegalArgumentException(context.getString(R.string.title_no_composable)); - - EntityIdentity identity = identities.get(0); - EntityFolder drafts = db.folder().getFolderByType(identity.account, EntityFolder.DRAFTS); - if (drafts == null) - throw new IllegalArgumentException(context.getString(R.string.title_no_drafts)); - - draft = new EntityMessage(); - draft.account = drafts.account; - draft.folder = drafts.id; - draft.identity = identity.id; - draft.msgid = EntityMessage.generateMessageId(); - draft.thread = draft.msgid; - draft.from = new Address[]{new InternetAddress(identity.email, identity.name, StandardCharsets.UTF_8.name())}; - draft.to = new Address[]{myAddress()}; - draft.subject = context.getString(R.string.app_name) + " " + getVersionInfo(context) + " debug info - " + source; - draft.received = new Date().getTime(); - draft.seen = true; - draft.ui_seen = true; - draft.id = db.message().insertMessage(draft); - - File file = draft.getFile(context); - Helper.writeText(file, body); // TODO CASA system info - db.message().setMessageContent(draft.id, true, null, 0, null, null); - - attachSettings(context, draft.id, 1); - attachAccounts(context, draft.id, 2); - attachNetworkInfo(context, draft.id, 3); - attachLog(context, draft.id, 4); - attachOperations(context, draft.id, 5); - attachTasks(context, draft.id, 6); - attachLogcat(context, draft.id, 7); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) - attachNotificationInfo(context, draft.id, 8); - attachEnvironment(context, draft.id, 9); - //if (MessageClassifier.isEnabled(context)) - // attachClassifierData(context, draft.id, 10); - - EntityOperation.queue(context, draft, EntityOperation.ADD); - - db.setTransactionSuccessful(); - } finally { - db.endTransaction(); - } - - ServiceSynchronize.eval(context, "debuginfo"); - - return draft; - } - static void unexpectedError(Fragment fragment, Throwable ex) { unexpectedError(fragment, ex, true); } @@ -2044,11 +1834,16 @@ public class Log { new SimpleTask() { @Override protected Long onExecute(Context context, Bundle args) throws Throwable { - return Log.getDebugInfo(context, "report", R.string.title_unexpected_info_remark, ex, null, null).id; + EntityMessage m = DebugHelper.getDebugInfo(context, + "report", R.string.title_unexpected_info_remark, ex, null, null); + return (m == null ? null : m.id); } @Override protected void onExecuted(Bundle args, Long id) { + if (id == null) + return; + context.startActivity(new Intent(context, ActivityCompose.class) .putExtra("action", "edit") .putExtra("id", id)); @@ -2066,1679 +1861,6 @@ public class Log { } } - private static String getVersionInfo(Context context) { - return String.format("%s%s/%d%s%s%s\r\n", - BuildConfig.VERSION_NAME, - BuildConfig.REVISION, - Helper.hasValidFingerprint(context) ? 1 : 3, - BuildConfig.PLAY_STORE_RELEASE ? "p" : "", - BuildConfig.DEBUG ? "d" : "", - ActivityBilling.isPro(context) ? "+" : "-"); - } - - private static StringBuilder getAppInfo(Context context) { - StringBuilder sb = new StringBuilder(); - - ContentResolver resolver = context.getContentResolver(); - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); - boolean main_log = prefs.getBoolean("main_log", true); - boolean protocol = prefs.getBoolean("protocol", false); - long last_cleanup = prefs.getLong("last_cleanup", 0); - - PackageManager pm = context.getPackageManager(); - - // Get version info - sb.append(String.format("%s %s\r\n", context.getString(R.string.app_name), getVersionInfo(context))); - sb.append(String.format("Package: %s uid: %d\r\n", - BuildConfig.APPLICATION_ID, android.os.Process.myUid())); - sb.append(String.format("Android: %s (SDK device=%d target=%d)\r\n", - Build.VERSION.RELEASE, Build.VERSION.SDK_INT, Helper.getTargetSdk(context))); - - String miui = Helper.getMIUIVersion(); - Integer autostart = (miui == null ? null : Helper.getMIUIAutostart(context)); - sb.append(String.format("MIUI: %s autostart: %s\r\n", - miui == null ? "-" : miui, - autostart == null ? "?" : Boolean.toString(autostart == 0))); - - boolean reporting = prefs.getBoolean("crash_reports", false); - if (reporting || BuildConfig.TEST_RELEASE) { - String uuid = prefs.getString("uuid", null); - sb.append(String.format("UUID: %s\r\n", uuid == null ? "-" : uuid)); - } - - try { - ApplicationInfo app = pm.getApplicationInfo(context.getPackageName(), PackageManager.GET_META_DATA); - String build_uuid = app.metaData.getString("com.bugsnag.android.BUILD_UUID"); - sb.append(String.format("Build UUID: %s\r\n", build_uuid == null ? "-" : build_uuid)); - } catch (PackageManager.NameNotFoundException ex) { - Log.e(ex); - } - - String installer = Helper.getInstallerName(context); - sb.append(String.format("Release: %s\r\n", getReleaseType(context))); - sb.append(String.format("Play Store: %s\r\n", Helper.hasPlayStore(context))); - sb.append(String.format("Installer: %s\r\n", installer == null ? "-" : installer)); - sb.append(String.format("Installed: %s\r\n", new Date(Helper.getInstallTime(context)))); - sb.append(String.format("Updated: %s\r\n", new Date(Helper.getUpdateTime(context)))); - sb.append(String.format("Last cleanup: %s\r\n", new Date(last_cleanup))); - sb.append(String.format("Now: %s\r\n", new Date())); - sb.append(String.format("Zone: %s\r\n", TimeZone.getDefault().getID())); - - String language = prefs.getString("language", null); - sb.append(String.format("Locale: def=%s lang=%s\r\n", - Locale.getDefault(), language)); - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) - sb.append(String.format("System: %s\r\n", - Resources.getSystem().getConfiguration().locale)); - else { - LocaleList ll = Resources.getSystem().getConfiguration().getLocales(); - for (int i = 0; i < ll.size(); i++) - sb.append(String.format("System: %s\r\n", ll.get(i))); - } - - sb.append("\r\n"); - - String osVersion = null; - try { - osVersion = System.getProperty("os.version"); - } catch (Throwable ex) { - Log.e(ex); - } - - // Get device info - sb.append(String.format("Brand: %s\r\n", Build.BRAND)); - sb.append(String.format("Manufacturer: %s\r\n", Build.MANUFACTURER)); - sb.append(String.format("Model: %s\r\n", Build.MODEL)); - sb.append(String.format("Product: %s\r\n", Build.PRODUCT)); - sb.append(String.format("Device: %s\r\n", Build.DEVICE)); - sb.append(String.format("Host: %s\r\n", Build.HOST)); - sb.append(String.format("Time: %s\r\n", new Date(Build.TIME).toString())); - sb.append(String.format("Display: %s\r\n", Build.DISPLAY)); - sb.append(String.format("Id: %s\r\n", Build.ID)); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) - sb.append(String.format("SoC: %s/%s\r\n", Build.SOC_MANUFACTURER, Build.SOC_MODEL)); - sb.append(String.format("OS version: %s\r\n", osVersion)); - sb.append("\r\n"); - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - try { - // https://developer.android.com/reference/android/app/ApplicationExitInfo - boolean exits = false; - long from = new Date().getTime() - 30 * 24 * 3600 * 1000L; - ActivityManager am = Helper.getSystemService(context, ActivityManager.class); - List infos = am.getHistoricalProcessExitReasons( - context.getPackageName(), 0, 100); - for (ApplicationExitInfo info : infos) - if (info.getTimestamp() > from && - info.getImportance() >= ActivityManager.RunningAppProcessInfo.IMPORTANCE_GONE) { - exits = true; - sb.append(String.format("%s: %s\r\n", - new Date(info.getTimestamp()), - Helper.getExitReason(info.getReason()))); - } - if (!exits) - sb.append("No crashes\r\n"); - sb.append("\r\n"); - } catch (Throwable ex) { - sb.append(ex).append("\r\n"); - } - } - - sb.append(String.format("Log main: %b protocol: %b build: %b test: %b\r\n", - main_log, protocol, BuildConfig.DEBUG, BuildConfig.TEST_RELEASE)); - - int[] contacts = ContactInfo.getStats(); - sb.append(String.format("Contact lookup: %d cached: %d\r\n", - contacts[0], contacts[1])); - - sb.append(String.format("Accessibility: %b\r\n", Helper.isAccessibilityEnabled(context))); - - String charset = MimeUtility.getDefaultJavaCharset(); - sb.append(String.format("Default charset: %s/%s\r\n", charset, MimeUtility.mimeCharset(charset))); - - String emoji; - try { - if (EmojiCompat.isConfigured()) { - int emojiState = EmojiCompat.get().getLoadState(); - switch (emojiState) { - case EmojiCompat.LOAD_STATE_LOADING: - emoji = "Loading"; - break; - case EmojiCompat.LOAD_STATE_SUCCEEDED: - emoji = "Loaded"; - break; - case EmojiCompat.LOAD_STATE_FAILED: - emoji = "Failed"; - break; - case EmojiCompat.LOAD_STATE_DEFAULT: - emoji = "Not loaded"; - break; - default: - emoji = "?" + emojiState; - } - } else - emoji = "Disabled"; - } catch (Throwable ex) { - Log.e(ex); - emoji = ex.toString(); - } - - sb.append("Emoji: ").append(emoji).append("\r\n"); - - sb.append("Transliterate: ") - .append(TextHelper.canTransliterate()) - .append("\r\n"); - - sb.append("Classifier: ") - .append(Helper.humanReadableByteCount(MessageClassifier.getSize(context))) - .append("\r\n"); - - sb.append("\r\n"); - - int cpus = Runtime.getRuntime().availableProcessors(); - sb.append(String.format("Processors: %d\r\n", cpus)); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - long running = SystemClock.uptimeMillis() - android.os.Process.getStartUptimeMillis(); - long cpu = android.os.Process.getElapsedCpuTime(); - int util = (int) (running == 0 ? 0 : 100 * cpu / running / cpus); - sb.append(String.format("Uptime: %s CPU: %s %d%%\r\n", - Helper.formatDuration(running), Helper.formatDuration(cpu), util)); - } - - Boolean largeHeap; - try { - ApplicationInfo info = pm.getApplicationInfo(context.getPackageName(), 0); - largeHeap = (info.flags & ApplicationInfo.FLAG_LARGE_HEAP) != 0; - } catch (Throwable ex) { - largeHeap = null; - } - - ActivityManager am = Helper.getSystemService(context, ActivityManager.class); - ActivityManager.MemoryInfo mi = new ActivityManager.MemoryInfo(); - am.getMemoryInfo(mi); - sb.append(String.format("Memory class: %d/%d Large: %s MB Total: %s Low: %b\r\n", - am.getMemoryClass(), am.getLargeMemoryClass(), - largeHeap == null ? "?" : Boolean.toString(largeHeap), - Helper.humanReadableByteCount(mi.totalMem), - am.isLowRamDevice())); - - long storage_available = Helper.getAvailableStorageSpace(); - long storage_total = Helper.getTotalStorageSpace(); - long storage_used = Helper.getSizeUsed(context.getFilesDir()); - sb.append(String.format("Storage space: %s/%s App: %s\r\n", - Helper.humanReadableByteCount(storage_total - storage_available), - Helper.humanReadableByteCount(storage_total), - Helper.humanReadableByteCount(storage_used))); - - long cache_used = Helper.getSizeUsed(context.getCacheDir()); - long cache_quota = Helper.getCacheQuota(context); - sb.append(String.format("Cache space: %s/%s\r\n", - Helper.humanReadableByteCount(cache_used), - Helper.humanReadableByteCount(cache_quota))); - - Runtime rt = Runtime.getRuntime(); - long hused = (rt.totalMemory() - rt.freeMemory()) / 1024L / 1024L; - long hmax = rt.maxMemory() / 1024L / 1024L; - long nheap = Debug.getNativeHeapAllocatedSize() / 1024L / 1024L; - long nsize = Debug.getNativeHeapSize() / 1024 / 1024L; - sb.append(String.format("Heap usage: %d/%d MiB native: %d/%d MiB\r\n", hused, hmax, nheap, nsize)); - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - int ipc = IBinder.getSuggestedMaxIpcSizeBytes(); - sb.append(String.format("IPC max: %s\r\n", Helper.humanReadableByteCount(ipc))); - } - - sb.append("\r\n"); - - WindowManager wm = Helper.getSystemService(context, WindowManager.class); - Display display = wm.getDefaultDisplay(); - Point dim = new Point(); - display.getSize(dim); - float density = context.getResources().getDisplayMetrics().density; - sb.append(String.format("Density 1dp=%f\r\n", density)); - sb.append(String.format("Resolution: %.2f x %.2f dp\r\n", dim.x / density, dim.y / density)); - - Configuration config = context.getResources().getConfiguration(); - - String size; - if (config.isLayoutSizeAtLeast(Configuration.SCREENLAYOUT_SIZE_XLARGE)) - size = "XLarge"; - else if (config.isLayoutSizeAtLeast(Configuration.SCREENLAYOUT_SIZE_LARGE)) - size = "Large"; - else if (config.isLayoutSizeAtLeast(Configuration.SCREENLAYOUT_SIZE_NORMAL)) - size = "Medium"; - else if (config.isLayoutSizeAtLeast(Configuration.SCREENLAYOUT_SIZE_SMALL)) - size = "Small"; - else - size = "size=" + (config.screenLayout & Configuration.SCREENLAYOUT_SIZE_MASK); - - String orientation; - if (config.orientation == Configuration.ORIENTATION_LANDSCAPE) - orientation = "Landscape"; - else if (config.orientation == Configuration.ORIENTATION_PORTRAIT) - orientation = "Portrait"; - else - orientation = "orientation=" + config.orientation; - - sb.append(String.format("%s %s\r\n", size, orientation)); - - try { - float animation_scale = Settings.Global.getFloat(resolver, - Settings.Global.WINDOW_ANIMATION_SCALE, 0f); - sb.append(String.format("Animation scale: %f %s\r\n", animation_scale, - animation_scale == 1f ? "" : "!!!")); - } catch (Throwable ex) { - sb.append(ex).append("\r\n"); - } - - int uiMode = context.getResources().getConfiguration().uiMode; - sb.append(String.format("UI mode: 0x")) - .append(Integer.toHexString(uiMode)) - .append(" night=").append(Helper.isNight(context)) - .append("\r\n"); - - String uiType = Helper.getUiModeType(context); - sb.append(String.format("UI type: %s %s\r\n", uiType, - "normal".equals(uiType) ? "" : "!!!")); - - sb.append(String.format("Darken support: %b\r\n", - WebViewEx.isFeatureSupported(context, WebViewFeature.ALGORITHMIC_DARKENING))); - try { - PackageInfo pkg = WebViewCompat.getCurrentWebViewPackage(context); - sb.append(String.format("WebView %d/%s\r\n", - pkg == null ? -1 : pkg.versionCode, - pkg == null ? null : pkg.versionName)); - } catch (Throwable ex) { - sb.append(ex).append("\r\n"); - } - - sb.append("\r\n"); - - Boolean ignoring = Helper.isIgnoringOptimizations(context); - sb.append(String.format("Battery optimizations: %s %s\r\n", - ignoring == null ? null : Boolean.toString(!ignoring), - Boolean.FALSE.equals(ignoring) ? "!!!" : "")); - - PowerManager power = Helper.getSystemService(context, PowerManager.class); - boolean psaving = power.isPowerSaveMode(); - sb.append(String.format("Battery saving: %s %s\r\n", psaving, psaving ? "!!!" : "")); - - sb.append(String.format("Charging: %b; level: %d\r\n", - Helper.isCharging(context), Helper.getBatteryLevel(context))); - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - // https://developer.android.com/reference/android/app/usage/UsageStatsManager - UsageStatsManager usm = Helper.getSystemService(context, UsageStatsManager.class); - int bucket = usm.getAppStandbyBucket(); - boolean inactive = usm.isAppInactive(BuildConfig.APPLICATION_ID); - sb.append(String.format("Standby bucket: %d-%b-%s %s\r\n", - bucket, inactive, Helper.getStandbyBucketName(bucket), - (bucket <= UsageStatsManager.STANDBY_BUCKET_ACTIVE && !inactive ? "" : "!!!"))); - } - - boolean canExact = AlarmManagerCompatEx.canScheduleExactAlarms(context); - boolean hasExact = AlarmManagerCompatEx.hasExactAlarms(context); - sb.append(String.format("ExactAlarms can=%b has=%b %s\r\n", canExact, hasExact, - canExact ? "" : "!!!")); - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - boolean restricted = am.isBackgroundRestricted(); - sb.append(String.format("Background restricted: %b %s\r\n", restricted, - restricted ? "!!!" : "")); - } - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - boolean saving = ConnectionHelper.isDataSaving(context); - sb.append(String.format("Data saving: %b %s\r\n", saving, - saving ? "!!!" : "")); - } - - try { - int finish_activities = Settings.Global.getInt(resolver, - Settings.Global.ALWAYS_FINISH_ACTIVITIES, 0); - sb.append(String.format("Always finish: %d %s\r\n", finish_activities, - finish_activities == 0 ? "" : "!!!")); - } catch (Throwable ex) { - sb.append(ex).append("\r\n"); - } - - sb.append("\r\n"); - - return sb; - } - - private static void attachSettings(Context context, long id, int sequence) { - try { - DB db = DB.getInstance(context); - - EntityAttachment attachment = new EntityAttachment(); - attachment.message = id; - attachment.sequence = sequence; - attachment.name = "settings.txt"; - attachment.type = "text/plain"; - attachment.disposition = Part.ATTACHMENT; - attachment.size = null; - attachment.progress = 0; - attachment.id = db.attachment().insertAttachment(attachment); - - long size = 0; - File file = attachment.getFile(context); - try (OutputStream os = new BufferedOutputStream(new FileOutputStream(file))) { - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); - - Map settings = prefs.getAll(); - List keys = new ArrayList<>(settings.keySet()); - Collections.sort(keys); - for (String key : keys) { - Object value = settings.get(key); - if ("wipe_mnemonic".equals(key) && value != null) - value = "[redacted]"; - else if ("cloud_user".equals(key) && value != null) - value = "[redacted]"; - else if ("cloud_password".equals(key) && value != null) - value = "[redacted]"; - else if ("pin".equals(key) && value != null) - value = "[redacted]"; - else if (key != null && key.startsWith("oauth.")) - value = "[redacted]"; - else if (key != null && key.startsWith("graph.contacts.")) - value = "[redacted]"; - size += write(os, key + "=" + value + "\r\n"); - } - - size += write(os, "\r\n"); - - try { - List names = new ArrayList<>(); - - Properties props = System.getProperties(); - Enumeration pnames = props.propertyNames(); - while (pnames.hasMoreElements()) - names.add((String) pnames.nextElement()); - - Collections.sort(names); - for (String name : names) - size += write(os, name + "=" + props.getProperty(name) + "\r\n"); - } catch (Throwable ex) { - size += write(os, ex.getMessage() + "\r\n"); - } - } - - db.attachment().setDownloaded(attachment.id, size); - } catch (Throwable ex) { - Log.e(ex); - } - } - - private static void attachAccounts(Context context, long id, int sequence) { - try { - DB db = DB.getInstance(context); - - EntityAttachment attachment = new EntityAttachment(); - attachment.message = id; - attachment.sequence = sequence; - attachment.name = "accounts.txt"; - attachment.type = "text/plain"; - attachment.disposition = Part.ATTACHMENT; - attachment.size = null; - attachment.progress = 0; - attachment.id = db.attachment().insertAttachment(attachment); - - DateFormat dtf = Helper.getDateTimeInstance(context, SimpleDateFormat.SHORT, SimpleDateFormat.SHORT); - - long size = 0; - File file = attachment.getFile(context); - try (OutputStream os = new BufferedOutputStream(new FileOutputStream(file))) { - List accounts = db.account().getAccounts(); - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); - boolean enabled = prefs.getBoolean("enabled", true); - int pollInterval = ServiceSynchronize.getPollInterval(context); - boolean metered = prefs.getBoolean("metered", true); - Boolean ignoring = Helper.isIgnoringOptimizations(context); - boolean canSchedule = AlarmManagerCompatEx.canScheduleExactAlarms(context); - boolean auto_optimize = prefs.getBoolean("auto_optimize", false); - boolean schedule = prefs.getBoolean("schedule", false); - - String ds = ConnectionHelper.getDataSaving(context); - boolean vpn = ConnectionHelper.vpnActive(context); - boolean ng = false; - try { - PackageManager pm = context.getPackageManager(); - pm.getPackageInfo("eu.faircode.netguard", 0); - ng = true; - } catch (Throwable ignored) { - } - - Integer bucket = null; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) - try { - UsageStatsManager usm = Helper.getSystemService(context, UsageStatsManager.class); - bucket = usm.getAppStandbyBucket(); - } catch (Throwable ignored) { - } - - Integer filter = null; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - NotificationManager nm = Helper.getSystemService(context, NotificationManager.class); - filter = nm.getCurrentInterruptionFilter(); - } - - StringBuilder filters = new StringBuilder(); - StringBuilder sorts = new StringBuilder(); - for (String key : prefs.getAll().keySet()) - if (key.startsWith("filter_")) { - Object value = prefs.getAll().get(key); - if (Boolean.TRUE.equals(value)) - filters.append(' ').append(key.substring(7)).append('=').append(value); - } else if (key.startsWith("sort_")) { - Object value = prefs.getAll().get(key); - sorts.append(' ').append(key).append('=').append(value); - } - - size += write(os, "enabled=" + enabled + (enabled ? "" : " !!!") + - " interval=" + pollInterval + "\r\n" + - "metered=" + metered + (metered ? "" : " !!!") + - " saving=" + ds + ("enabled".equals(ds) ? " !!!" : "") + - " vpn=" + vpn + (vpn ? " !!!" : "") + - " ng=" + ng + "\r\n" + - "optimizing=" + (ignoring == null ? null : !ignoring) + (Boolean.FALSE.equals(ignoring) ? " !!!" : "") + - " bucket=" + (bucket == null ? null : - Helper.getStandbyBucketName(bucket) + - (bucket > UsageStatsManager.STANDBY_BUCKET_ACTIVE ? " !!!" : "")) + - " canSchedule=" + canSchedule + (canSchedule ? "" : " !!!") + - " auto_optimize=" + auto_optimize + (auto_optimize ? " !!!" : "") + - " notifications=" + (filter == null ? null : - Helper.getInterruptionFilter(filter) + - (filter == NotificationManager.INTERRUPTION_FILTER_ALL ? "" : " !!!")) + "\r\n" + - "accounts=" + accounts.size() + - " folders=" + db.folder().countSync() + "/" + db.folder().countTotal() + - " messages=" + db.message().countTotal() + - " rules=" + db.rule().countTotal() + - " ops=" + db.operation().getOperationCount() + - " outbox=" + db.message().countOutbox() + "\r\n" + - "filter " + filters + " " + sorts + - "\r\n\r\n"); - - if (schedule) { - int minuteStart = prefs.getInt("schedule_start", 0); - int minuteEnd = prefs.getInt("schedule_end", 0); - int minuteStartWeekend = prefs.getInt("schedule_start_weekend", minuteStart); - int minuteEndWeekend = prefs.getInt("schedule_end_weekend", minuteEnd); - - size += write(os, String.format("schedule %s...%s weekend %s...%s\r\n", - CalendarHelper.formatHour(context, minuteStart), - CalendarHelper.formatHour(context, minuteEnd), - CalendarHelper.formatHour(context, minuteStartWeekend), - CalendarHelper.formatHour(context, minuteEndWeekend))); - - String[] daynames = new DateFormatSymbols().getWeekdays(); - for (int i = 0; i < 7; i++) { - boolean day = prefs.getBoolean("schedule_day" + i, true); - boolean weekend = CalendarHelper.isWeekend(context, i + 1); - size += write(os, String.format("schedule %s=%b %s\r\n", - daynames[i + 1], day, weekend ? "weekend" : "")); - } - - size += write(os, "\r\n"); - } - - for (EntityAccount account : accounts) - if (account.synchronize) - try { - String info = "pwd"; - if (account.auth_type == ServiceAuthenticator.AUTH_TYPE_OAUTH || - account.auth_type == ServiceAuthenticator.AUTH_TYPE_GRAPH) - info = getTokenInfo(account.password, account.auth_type); - size += write(os, String.format("%s %s\r\n", account.name, info)); - - List identities = db.identity().getSynchronizingIdentities(account.id); - for (EntityIdentity identity : identities) - if (identity.auth_type == ServiceAuthenticator.AUTH_TYPE_OAUTH || - identity.auth_type == ServiceAuthenticator.AUTH_TYPE_GRAPH) - size += write(os, String.format("- %s %s\r\n", - identity.name, getTokenInfo(identity.password, identity.auth_type))); - } catch (Throwable ex) { - size += write(os, ex.toString() + "\r\n"); - } - - size += write(os, "\r\n"); - - Map unified = new HashMap<>(); - for (EntityFolder folder : db.folder().getFoldersByType(EntityFolder.INBOX)) - unified.put(folder.id, folder); - for (EntityFolder folder : db.folder().getFoldersUnified(null, false)) - unified.put(folder.id, folder); - - for (Long fid : unified.keySet()) { - EntityFolder folder = unified.get(fid); - EntityAccount account = db.account().getAccount(folder.account); - size += write(os, String.format("%s/%s:%s sync=%b unified=%b\r\n", - (account == null ? null : account.name), - folder.name, folder.type, folder.synchronize, folder.unified)); - } - - size += write(os, "\r\n"); - - for (EntityAccount account : accounts) { - if (account.synchronize) { - int content = 0; - int messages = 0; - List folders = db.folder().getFoldersEx(account.id); - for (TupleFolderEx folder : folders) { - content += folder.content; - messages += folder.messages; - } - - int blocked = db.contact().countBlocked(account.id); - - boolean unmetered = false; - boolean ignore_schedule = false; - try { - if (account.conditions != null) { - JSONObject jconditions = new JSONObject(account.conditions); - unmetered = jconditions.optBoolean("unmetered"); - ignore_schedule = jconditions.optBoolean("ignore_schedule"); - } - } catch (Throwable ignored) { - } - - size += write(os, account.id + ":" + account.name + (account.primary ? "*" : "") + - " " + (account.protocol == EntityAccount.TYPE_IMAP ? "IMAP" : "POP") + - " [" + (account.provider == null ? "" : account.provider) + - ":" + ServiceAuthenticator.getAuthTypeName(account.auth_type) + "]" + - " " + account.host + ":" + account.port + "/" + - EmailService.getEncryptionName(account.encryption) + - (account.insecure ? " !!!" : "") + - " sync=" + account.synchronize + - " exempted=" + account.poll_exempted + (pollInterval > 0 && account.poll_exempted ? " !!!" : "") + - " poll=" + account.poll_interval + - " ondemand=" + account.ondemand + (account.ondemand ? " !!!" : "") + - " msgs=" + content + "/" + messages + " max=" + account.max_messages + - " blocked=" + blocked + (blocked == 0 ? "" : " !!!") + - " ops=" + db.operation().getOperationCount(account.id) + - " schedule=" + (!ignore_schedule) + (ignore_schedule ? " !!!" : "") + - " unmetered=" + unmetered + (unmetered ? " !!!" : "") + - " quota=" + (account.quota_usage == null ? "-" : Helper.humanReadableByteCount(account.quota_usage)) + - "/" + (account.quota_limit == null ? "-" : Helper.humanReadableByteCount(account.quota_limit)) + - " " + account.state + - (account.last_connected == null ? "" : " " + dtf.format(account.last_connected)) + - (account.error == null ? "" : "\r\n" + account.error) + - "\r\n"); - - if (folders.size() > 0) - Collections.sort(folders, folders.get(0).getComparator(context)); - for (TupleFolderEx folder : folders) - if (folder.synchronize || account.protocol == EntityAccount.TYPE_POP) { - int unseen = db.message().countUnseen(folder.id); - int hidden = db.message().countHidden(folder.id); - int notifying = db.message().countNotifying(folder.id); - size += write(os, "- " + folder.id + ":" + folder.name + " " + - folder.type + (folder.inherited_type == null ? "" : "/" + folder.inherited_type) + - (folder.unified ? " unified" : "") + - (folder.notify ? " notify" : "") + - (Boolean.TRUE.equals(folder.subscribed) ? " subscribed" : "") + - " poll=" + folder.poll + (folder.poll || EntityFolder.INBOX.equals(folder.type) ? "" : " !!! ") + - " factor=" + folder.poll_factor + - " days=" + getDays(folder.sync_days) + "/" + getDays(folder.keep_days) + - " msgs=" + folder.content + "/" + folder.messages + "/" + folder.total + - " ops=" + db.operation().getOperationCount(folder.id, null) + - " unseen=" + unseen + " hidden=" + hidden + " notifying=" + notifying + - " " + folder.state + - (folder.last_sync == null ? "" : " " + dtf.format(folder.last_sync)) + - "\r\n"); - } - - List swipes = db.account().getAccountSwipes(account.id); - if (swipes == null) - size += write(os, "<> swipes?\r\n"); - else - for (TupleAccountSwipes swipe : swipes) { - size += write(os, "> " + EntityMessage.getSwipeType(swipe.swipe_left) + " " + - swipe.left_name + ":" + swipe.left_type + "\r\n"); - size += write(os, "< " + EntityMessage.getSwipeType(swipe.swipe_right) + " " + - swipe.right_name + ":" + swipe.right_type + "\r\n"); - } - - size += write(os, "\r\n"); - } - } - - for (EntityAccount account : accounts) - if (account.synchronize) { - List identities = db.identity().getIdentities(account.id); - for (EntityIdentity identity : identities) - if (identity.synchronize) { - size += write(os, account.name + "/" + identity.name + (identity.primary ? "*" : "") + " " + - identity.display + " " + identity.email + - (identity.self ? "" : " !self") + - " [" + (identity.provider == null ? "" : identity.provider) + - ":" + ServiceAuthenticator.getAuthTypeName(identity.auth_type) + "]" + - (TextUtils.isEmpty(identity.sender_extra_regex) ? "" : " regex=" + identity.sender_extra_regex) + - (!identity.sender_extra ? "" : " edit" + - (identity.sender_extra_name ? "+name" : "-name") + - (identity.reply_extra_name ? "+copy" : "-copy")) + - " " + identity.host + ":" + identity.port + "/" + - EmailService.getEncryptionName(identity.encryption) + - (identity.insecure ? " !!!" : "") + - " ops=" + db.operation().getOperationCount(EntityOperation.SEND) + - " max=" + (identity.max_size == null ? "-" : Helper.humanReadableByteCount(identity.max_size)) + - " " + identity.state + - (identity.last_connected == null ? "" : " " + dtf.format(identity.last_connected)) + - (identity.error == null ? "" : "\r\n" + identity.error) + - "\r\n"); - } - } - - size += write(os, "\r\n"); - - for (EntityAccount account : accounts) { - int ops = db.operation().getOperationCount(account.id); - if (account.synchronize || ops > 0) - try { - JSONObject jaccount = account.toJSON(); - jaccount.put("state", account.state == null ? "null" : account.state); - jaccount.put("warning", account.warning); - jaccount.put("operations", ops); - jaccount.put("error", account.error); - jaccount.put("capabilities", account.capabilities); - - if (account.last_connected != null) - jaccount.put("last_connected", new Date(account.last_connected).toString()); - - jaccount.put("keep_alive_ok", account.keep_alive_ok); - jaccount.put("keep_alive_failed", account.keep_alive_failed); - jaccount.put("keep_alive_succeeded", account.keep_alive_succeeded); - - jaccount.remove("password"); - - size += write(os, "==========\r\n"); - size += write(os, jaccount.toString(2) + "\r\n"); - - List folders = db.folder().getFolders(account.id, false, false); - if (folders.size() > 0) - Collections.sort(folders, folders.get(0).getComparator(context)); - for (EntityFolder folder : folders) { - JSONObject jfolder = folder.toJSON(); - jfolder.put("inherited_type", folder.inherited_type); - jfolder.put("level", folder.level); - jfolder.put("total", folder.total); - jfolder.put("initialize", folder.initialize); - jfolder.put("subscribed", folder.subscribed); - jfolder.put("state", folder.state == null ? "null" : folder.state); - jfolder.put("sync_state", folder.sync_state == null ? "null" : folder.sync_state); - jfolder.put("poll_count", folder.poll_count); - jfolder.put("read_only", folder.read_only); - jfolder.put("selectable", folder.selectable); - jfolder.put("inferiors", folder.inferiors); - jfolder.put("auto_add", folder.auto_add); - jfolder.put("flags", folder.flags == null ? null : TextUtils.join(",", folder.flags)); - jfolder.put("keywords", folder.keywords == null ? null : TextUtils.join(",", folder.keywords)); - jfolder.put("tbc", Boolean.TRUE.equals(folder.tbc)); - jfolder.put("rename", folder.rename); - jfolder.put("tbd", Boolean.TRUE.equals(folder.tbd)); - jfolder.put("operations", db.operation().getOperationCount(folder.id, null)); - jfolder.put("error", folder.error); - if (folder.last_sync != null) - jfolder.put("last_sync", new Date(folder.last_sync).toString()); - if (folder.last_sync_count != null) - jfolder.put("last_sync_count", folder.last_sync_count); - size += write(os, jfolder.toString(2) + "\r\n"); - } - - List identities = db.identity().getIdentities(account.id); - for (EntityIdentity identity : identities) - try { - JSONObject jidentity = identity.toJSON(); - jidentity.remove("password"); - jidentity.remove("signature"); - size += write(os, "----------\r\n"); - size += write(os, jidentity.toString(2) + "\r\n"); - } catch (JSONException ex) { - size += write(os, ex.toString() + "\r\n"); - } - } catch (JSONException ex) { - size += write(os, ex.toString() + "\r\n"); - } - } - } - - db.attachment().setDownloaded(attachment.id, size); - } catch (Throwable ex) { - Log.e(ex); - } - } - - private static String getDays(Integer days) { - if (days == null) - return "?"; - else - return (days == Integer.MAX_VALUE ? "∞" : Integer.toString(days)); - } - - private static void attachNetworkInfo(Context context, long id, int sequence) { - try { - DB db = DB.getInstance(context); - - EntityAttachment attachment = new EntityAttachment(); - attachment.message = id; - attachment.sequence = sequence; - attachment.name = "network.txt"; - attachment.type = "text/plain"; - attachment.disposition = Part.ATTACHMENT; - attachment.size = null; - attachment.progress = 0; - attachment.id = db.attachment().insertAttachment(attachment); - - Boolean isValidated = null; - Boolean isCaptive = null; - - long size = 0; - File file = attachment.getFile(context); - try (OutputStream os = new BufferedOutputStream(new FileOutputStream(file))) { - ConnectivityManager cm = Helper.getSystemService(context, ConnectivityManager.class); - - NetworkInfo ani = cm.getActiveNetworkInfo(); - if (ani != null) - size += write(os, "Active network info=" + ani + - " connecting=" + ani.isConnectedOrConnecting() + - " connected=" + ani.isConnected() + - " available=" + ani.isAvailable() + - " state=" + ani.getState() + "/" + ani.getDetailedState() + - " metered=" + cm.isActiveNetworkMetered() + - " roaming=" + ani.isRoaming() + - " type=" + ani.getType() + "/" + ani.getTypeName() + - "\r\n\r\n"); - - Network active = ConnectionHelper.getActiveNetwork(context); - NetworkInfo a = (active == null ? null : cm.getNetworkInfo(active)); - NetworkCapabilities c = (active == null ? null : cm.getNetworkCapabilities(active)); - LinkProperties p = (active == null ? null : cm.getLinkProperties(active)); - boolean n = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M); - size += write(os, "Active network=" + active + " native=" + n + "\r\n"); - size += write(os, " info=" + a + - " connecting=" + (a == null ? null : a.isConnectedOrConnecting()) + - " connected=" + (a == null ? null : a.isConnected()) + - " available=" + (a == null ? null : a.isAvailable()) + - " state=" + (a == null ? null : a.getState() + "/" + a.getDetailedState()) + - " roaming=" + (a == null ? null : a.isRoaming()) + - " type=" + (a == null ? null : a.getType() + "/" + a.getTypeName()) + - "\r\n"); - size += write(os, " caps=" + c + "\r\n"); - size += write(os, " props=" + p + "\r\n\r\n"); - - for (Network network : cm.getAllNetworks()) { - size += write(os, (network.equals(active) ? "active=" : "network=") + network + "\r\n"); - - NetworkCapabilities caps = cm.getNetworkCapabilities(network); - size += write(os, " caps=" + caps + "\r\n"); - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - if (isValidated == null) - isValidated = false; - if (caps != null && caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)) - isValidated = true; - - if (isCaptive == null) - isCaptive = false; - if (caps != null && caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_CAPTIVE_PORTAL)) - isCaptive = true; - } - - LinkProperties props = cm.getLinkProperties(network); - size += write(os, " props=" + props + "\r\n"); - - size += write(os, "\r\n"); - } - - try { - Enumeration interfaces = NetworkInterface.getNetworkInterfaces(); - while (interfaces != null && interfaces.hasMoreElements()) { - NetworkInterface ni = interfaces.nextElement(); - size += write(os, "Interface=" + ni + "\r\n"); - for (InterfaceAddress iaddr : ni.getInterfaceAddresses()) { - InetAddress addr = iaddr.getAddress(); - size += write(os, " addr=" + addr + - (addr.isLoopbackAddress() ? " loopback" : "") + - (addr.isSiteLocalAddress() ? " site local (LAN)" : "") + - (addr.isLinkLocalAddress() ? " link local (device)" : "") + - (addr.isAnyLocalAddress() ? " any local" : "") + - (addr.isMulticastAddress() ? " multicast" : "") + "\r\n"); - } - size += write(os, "\r\n"); - } - } catch (Throwable ex) { - size += write(os, ex.getMessage() + "\r\n"); - } - - ConnectionHelper.NetworkState state = ConnectionHelper.getNetworkState(context); - size += write(os, "Connected=" + state.isConnected() + "\r\n"); - size += write(os, "Suitable=" + state.isSuitable() + "\r\n"); - size += write(os, "Unmetered=" + state.isUnmetered() + "\r\n"); - size += write(os, "Roaming=" + state.isRoaming() + "\r\n"); - size += write(os, "\r\n"); - - boolean[] has46 = ConnectionHelper.has46(context); - - size += write(os, "Has IPv4=" + has46[0] + " IPv6=" + has46[1] + "\r\n"); - size += write(os, "VPN active=" + ConnectionHelper.vpnActive(context) + "\r\n"); - size += write(os, "Data saving=" + ConnectionHelper.isDataSaving(context) + "\r\n"); - size += write(os, "Airplane=" + ConnectionHelper.airplaneMode(context) + "\r\n"); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) - size += write(os, "Cleartext permitted= " + - NetworkSecurityPolicy.getInstance().isCleartextTrafficPermitted() + "\r\n"); - size += write(os, "\r\n"); - - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); - int timeout = prefs.getInt("timeout", EmailService.DEFAULT_CONNECT_TIMEOUT); - boolean metered = prefs.getBoolean("metered", true); - int download = prefs.getInt("download", MessageHelper.DEFAULT_DOWNLOAD_SIZE); - boolean download_limited = prefs.getBoolean("download_limited", false); - boolean roaming = prefs.getBoolean("roaming", true); - boolean rlah = prefs.getBoolean("rlah", true); - boolean download_headers = prefs.getBoolean("download_headers", false); - boolean download_eml = prefs.getBoolean("download_eml", false); - boolean download_plain = prefs.getBoolean("download_plain", false); - boolean standalone_vpn = prefs.getBoolean("standalone_vpn", false); - boolean require_validated = prefs.getBoolean("require_validated", false); - boolean require_validated_captive = prefs.getBoolean("require_validated_captive", true); - boolean vpn_only = prefs.getBoolean("vpn_only", false); - boolean tcp_keep_alive = prefs.getBoolean("tcp_keep_alive", false); - boolean ssl_harden = prefs.getBoolean("ssl_harden", false); - boolean ssl_harden_strict = (ssl_harden && prefs.getBoolean("ssl_harden_strict", false)); - boolean cert_strict = prefs.getBoolean("cert_strict", true); - boolean open_safe = prefs.getBoolean("open_safe", false); - - size += write(os, "timeout=" + timeout + "s" + (timeout == EmailService.DEFAULT_CONNECT_TIMEOUT ? "" : " !!!") + "\r\n"); - size += write(os, "metered=" + metered + (metered ? "" : " !!!") + "\r\n"); - size += write(os, "download=" + Helper.humanReadableByteCount(download) + - " unmetered=" + download_limited + (download_limited ? " !!!" : "") + "\r\n"); - size += write(os, "roaming=" + roaming + (roaming ? "" : " !!!") + "\r\n"); - size += write(os, "rlah=" + rlah + (rlah ? "" : " !!!") + "\r\n"); - - size += write(os, "headers=" + download_headers + (download_headers ? " !!!" : "") + "\r\n"); - size += write(os, "eml=" + download_eml + (download_eml ? " !!!" : "") + "\r\n"); - size += write(os, "plain=" + download_plain + (download_plain ? " !!!" : "") + "\r\n"); - - size += write(os, "captive=" + (isCaptive == null ? "-" : Boolean.toString(isCaptive)) + "\r\n"); - size += write(os, "validation=" + require_validated + (require_validated ? " !!!" : "") + - " captive=" + require_validated_captive + (require_validated_captive ? "" : " !!!") + "\r\n"); - size += write(os, "validated=" + (isValidated == null ? "-" : Boolean.toString(isValidated)) + - (Boolean.FALSE.equals(isValidated) && - (Boolean.TRUE.equals(isCaptive) ? require_validated_captive : require_validated) ? " !!!" : "") + "\r\n"); - - size += write(os, "standalone_vpn=" + standalone_vpn + (standalone_vpn ? " !!!" : "") + "\r\n"); - size += write(os, "vpn_only=" + vpn_only + (vpn_only ? " !!!" : "") + "\r\n"); - - size += write(os, "tcp_keep_alive=" + tcp_keep_alive + (tcp_keep_alive ? " !!!" : "") + "\r\n"); - size += write(os, "ssl_harden=" + ssl_harden + (ssl_harden ? " !!!" : "") + "\r\n"); - size += write(os, "ssl_harden_strict=" + ssl_harden_strict + (ssl_harden_strict ? " !!!" : "") + "\r\n"); - size += write(os, "cert_strict=" + cert_strict + (cert_strict ? " !!!" : "") + "\r\n"); - size += write(os, "open_safe=" + open_safe + "\r\n"); - - size += write(os, "\r\n"); - size += write(os, getCiphers().toString()); - - try { - String algo = TrustManagerFactory.getDefaultAlgorithm(); - TrustManagerFactory tmf = TrustManagerFactory.getInstance(algo); - tmf.init((KeyStore) null); - - TrustManager[] tms = tmf.getTrustManagers(); - if (tms != null) - for (TrustManager tm : tms) { - size += write(os, String.format("Trust manager: %s (%s)\n", - tm.getClass().getName(), algo)); - if (tm instanceof X509TrustManager) - for (X509Certificate cert : ((X509TrustManager) tm).getAcceptedIssuers()) - size += write(os, String.format("- %s\n", cert.getIssuerDN())); - } - } catch (Throwable ex) { - size += write(os, ex.getMessage() + "\r\n"); - } - } - - db.attachment().setDownloaded(attachment.id, size); - } catch (Throwable ex) { - Log.e(ex); - } - } - - private static void attachLog(Context context, long id, int sequence) { - try { - DB db = DB.getInstance(context); - - EntityAttachment attachment = new EntityAttachment(); - attachment.message = id; - attachment.sequence = sequence; - attachment.name = "log.txt"; - attachment.type = "text/plain"; - attachment.disposition = Part.ATTACHMENT; - attachment.size = null; - attachment.progress = 0; - attachment.id = db.attachment().insertAttachment(attachment); - - long size = 0; - File file = attachment.getFile(context); - try (OutputStream os = new BufferedOutputStream(new FileOutputStream(file))) { - long from = new Date().getTime() - 24 * 3600 * 1000L; - DateFormat TF = Helper.getTimeInstance(context); - - for (EntityLog entry : db.log().getLogs(from, null)) - if (entry.data != null && entry.data.contains("backoff=")) - size += write(os, String.format("%s %s\r\n", - TF.format(entry.time), - entry.data)); - - size += write(os, "\r\n"); - - for (EntityLog entry : db.log().getLogs(from, null)) { - size += write(os, String.format("%s [%d:%d:%d:%d:%d] %s\r\n", - TF.format(entry.time), - entry.type.ordinal(), - (entry.thread == null ? 0 : entry.thread), - (entry.account == null ? 0 : entry.account), - (entry.folder == null ? 0 : entry.folder), - (entry.message == null ? 0 : entry.message), - entry.data)); - if (size > MAX_LOG_SIZE) { - size += write(os, "\r\n"); - break; - } - } - } - - db.attachment().setDownloaded(attachment.id, size); - if (!BuildConfig.DEBUG && size > MIN_ZIP_SIZE) - attachment.zip(context); - } catch (Throwable ex) { - Log.e(ex); - } - } - - private static void attachOperations(Context context, long id, int sequence) { - try { - DB db = DB.getInstance(context); - - EntityAttachment attachment = new EntityAttachment(); - attachment.message = id; - attachment.sequence = sequence; - attachment.name = "operations.txt"; - attachment.type = "text/plain"; - attachment.disposition = Part.ATTACHMENT; - attachment.size = null; - attachment.progress = 0; - attachment.id = db.attachment().insertAttachment(attachment); - - long size = 0; - File file = attachment.getFile(context); - try (OutputStream os = new BufferedOutputStream(new FileOutputStream(file))) { - DateFormat TF = Helper.getTimeInstance(context); - - for (EntityOperation op : db.operation().getOperations()) { - EntityAccount account = (op.account == null ? null : db.account().getAccount(op.account)); - EntityFolder folder = (op.folder == null ? null : db.folder().getFolder(op.folder)); - size += write(os, String.format("%s %s/%s %d %s/%d %s %s %s\r\n", - TF.format(op.created), - account == null ? null : account.name, - folder == null ? null : folder.name, - op.message == null ? -1 : op.message, - op.name, - op.tries, - op.args, - op.state, - op.error)); - } - } - - db.attachment().setDownloaded(attachment.id, size); - } catch (Throwable ex) { - Log.e(ex); - } - } - - private static void attachTasks(Context context, long id, int sequence) { - try { - DB db = DB.getInstance(context); - - EntityAttachment attachment = new EntityAttachment(); - attachment.message = id; - attachment.sequence = sequence; - attachment.name = "tasks.txt"; - attachment.type = "text/plain"; - attachment.disposition = Part.ATTACHMENT; - attachment.size = null; - attachment.progress = 0; - attachment.id = db.attachment().insertAttachment(attachment); - - long size = 0; - File file = attachment.getFile(context); - try (OutputStream os = new BufferedOutputStream(new FileOutputStream(file))) { - for (SimpleTask task : SimpleTask.getList()) - size += write(os, String.format("%s\r\n", task.toString())); - size += write(os, "\r\n"); - for (TwoStateOwner owner : TwoStateOwner.getList()) - size += write(os, String.format("%s\r\n", owner.toString())); - } - - db.attachment().setDownloaded(attachment.id, size); - } catch (Throwable ex) { - Log.e(ex); - } - } - - private static void attachLogcat(Context context, long id, int sequence) { - try { - DB db = DB.getInstance(context); - - EntityAttachment attachment = new EntityAttachment(); - attachment.message = id; - attachment.sequence = sequence; - attachment.name = "logcat.txt"; - attachment.type = "text/plain"; - attachment.disposition = Part.ATTACHMENT; - attachment.size = null; - attachment.progress = 0; - attachment.id = db.attachment().insertAttachment(attachment); - - attachment.zip(context, TinyLogConfigurationLoader.getFiles(context)); -/* - // https://cheatsheetseries.owasp.org/cheatsheets/OS_Command_Injection_Defense_Cheat_Sheet.html#java - ProcessBuilder pb = new ProcessBuilder("/system/bin/logcat", - "-d", - "-v", "threadtime", - //"-t", "1000", - Log.TAG + ":I"); - Map env = pb.environment(); - env.clear(); - pb.directory(context.getFilesDir()); - - Process proc = null; - File file = attachment.getFile(context); - try (OutputStream os = new BufferedOutputStream(new FileOutputStream(file))) { - proc = pb.start(); - - long size = 0; - try (BufferedReader br = new BufferedReader(new InputStreamReader(proc.getInputStream()))) { - String line; - while ((line = br.readLine()) != null) - size += write(os, line + "\r\n"); - } - - db.attachment().setDownloaded(attachment.id, size); - if (!BuildConfig.DEBUG && size > MIN_ZIP_SIZE) - attachment.zip(context); - } finally { - if (proc != null) - proc.destroy(); - } -*/ - } catch (Throwable ex) { - Log.e(ex); - } - } - - @RequiresApi(api = Build.VERSION_CODES.O) - private static void attachNotificationInfo(Context context, long id, int sequence) { - try { - DB db = DB.getInstance(context); - - EntityAttachment attachment = new EntityAttachment(); - attachment.message = id; - attachment.sequence = sequence; - attachment.name = "notification.txt"; - attachment.type = "text/plain"; - attachment.disposition = Part.ATTACHMENT; - attachment.size = null; - attachment.progress = 0; - attachment.id = db.attachment().insertAttachment(attachment); - - long size = 0; - File file = attachment.getFile(context); - try (OutputStream os = new BufferedOutputStream(new FileOutputStream(file))) { - NotificationManager nm = Helper.getSystemService(context, NotificationManager.class); - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - boolean permission = Helper.hasPermission(context, Manifest.permission.POST_NOTIFICATIONS); - boolean enabled = nm.areNotificationsEnabled(); - size += write(os, String.format("Permission=%b %s Enabled=%b %s\r\n", - permission, (permission ? "" : "!!!"), - enabled, (enabled ? "" : "!!!"))); - } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - boolean paused = nm.areNotificationsPaused(); - size += write(os, String.format("Paused=%b %s\r\n", - paused, (paused ? "!!!" : ""))); - } - - int filter = nm.getCurrentInterruptionFilter(); - size += write(os, String.format("Interruption filter allow=%s %s\r\n\r\n", - Helper.getInterruptionFilter(filter), - (filter == NotificationManager.INTERRUPTION_FILTER_ALL ? "" : "!!!"))); - - size += write(os, String.format("InCall=%b DND=%b\r\n\r\n", - MediaPlayerHelper.isInCall(context), - MediaPlayerHelper.isDnd(context))); - - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); - - StringBuilder options = new StringBuilder(); - for (String key : prefs.getAll().keySet()) - if (key.startsWith("notify_")) { - Object value = prefs.getAll().get(key); - boolean mark = false; - if ("notify_known".equals(key) && Boolean.TRUE.equals(value)) - mark = true; - if ("notify_background_only".equals(key) && Boolean.TRUE.equals(value)) - mark = true; - if ("notify_suppress_in_car".equals(key) && Boolean.TRUE.equals(value)) - mark = true; - options.append(' ').append(key).append('=') - .append(value) - .append(mark ? " !!!" : "") - .append("\r\n"); - } - - if (options.length() > 0) { - options.append("\r\n"); - size += write(os, options.toString()); - } - - for (NotificationChannel channel : nm.getNotificationChannels()) - try { - JSONObject jchannel = NotificationHelper.channelToJSON(channel); - size += write(os, jchannel.toString(2) + "\r\n\r\n"); - } catch (JSONException ex) { - size += write(os, ex + "\r\n"); - } - - size += write(os, - String.format("Importance none=%d; min=%d; low=%d; default=%d; high=%d; max=%d; unspecified=%d\r\n", - NotificationManager.IMPORTANCE_NONE, - NotificationManager.IMPORTANCE_MIN, - NotificationManager.IMPORTANCE_LOW, - NotificationManager.IMPORTANCE_DEFAULT, - NotificationManager.IMPORTANCE_HIGH, - NotificationManager.IMPORTANCE_MAX, - NotificationManager.IMPORTANCE_UNSPECIFIED)); - size += write(os, - String.format("Visibility private=%d; public=%d; secret=%d\r\n", - Notification.VISIBILITY_PRIVATE, - Notification.VISIBILITY_PUBLIC, - Notification.VISIBILITY_SECRET)); - size += write(os, String.format("Interruption filter\r\n")); - size += write(os, String.format("- All: no notifications are suppressed.\r\n")); - size += write(os, String.format("- Priority: all notifications are suppressed except those that match the priority criteria. Some audio streams are muted.\r\n")); - size += write(os, String.format("- None: all notifications are suppressed and all audio streams (except those used for phone calls) and vibrations are muted.\r\n")); - size += write(os, String.format("- Alarm: all notifications except those of category alarm are suppressed. Some audio streams are muted.\r\n")); - } - - db.attachment().setDownloaded(attachment.id, size); - } catch (Throwable ex) { - Log.e(ex); - } - } - - private static void attachEnvironment(Context context, long id, int sequence) { - try { - DB db = DB.getInstance(context); - - EntityAttachment attachment = new EntityAttachment(); - attachment.message = id; - attachment.sequence = sequence; - attachment.name = "environment.txt"; - attachment.type = "text/plain"; - attachment.disposition = Part.ATTACHMENT; - attachment.size = null; - attachment.progress = 0; - attachment.id = db.attachment().insertAttachment(attachment); - - long now = new Date().getTime(); - PackageManager pm = context.getPackageManager(); - - long size = 0; - File file = attachment.getFile(context); - try (OutputStream os = new BufferedOutputStream(new FileOutputStream(file))) { - size += write(os, String.format("Photo picker=%b\r\n", Helper.hasPhotoPicker())); - size += write(os, String.format("Double tap timeout=%d\r\n", ViewConfiguration.getDoubleTapTimeout())); - size += write(os, String.format("Long press timeout=%d\r\n", ViewConfiguration.getLongPressTimeout())); - - for (Class cls : new Class[]{ - ActivitySendSelf.class, - ActivitySearch.class, - ActivityAnswer.class, - ReceiverAutoStart.class}) - size += write(os, String.format("%s=%b\r\n", - cls.getSimpleName(), Helper.isComponentEnabled(context, cls))); - size += write(os, "\r\n"); - - try { - ApplicationInfo app = pm.getApplicationInfo(context.getPackageName(), PackageManager.GET_META_DATA); - List metas = getExtras(app.metaData); - size += write(os, "Manifest metas=" + (metas == null ? null : metas.size()) + "\r\n"); - for (String meta : metas) - size += write(os, String.format("%s\r\n", meta)); - } catch (Throwable ex) { - size += write(os, String.format("%s\r\n", ex)); - } - size += write(os, "\r\n"); - - int flags = PackageManager.GET_RESOLVED_FILTER; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) - flags |= PackageManager.MATCH_ALL; - - try { - Intent home = new Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_HOME); - List homes = context.getPackageManager().queryIntentActivities(home, PackageManager.MATCH_DEFAULT_ONLY); - size += write(os, "Launchers=" + (homes == null ? null : homes.size()) + "\r\n"); - if (homes != null) - for (ResolveInfo ri : homes) - size += write(os, String.format("Launcher=%s\r\n", ri.activityInfo.packageName)); - - ResolveInfo rid = context.getPackageManager().resolveActivity(home, PackageManager.MATCH_DEFAULT_ONLY); - size += write(os, String.format("Default launcher=%s\r\n", (rid == null ? null : rid.activityInfo.packageName))); - } catch (Throwable ex) { - size += write(os, String.format("%s\r\n", ex)); - } - size += write(os, "\r\n"); - - try { - Intent intent = new Intent(Intent.ACTION_VIEW) - //.addCategory(Intent.CATEGORY_BROWSABLE) - .setData(Uri.parse("http://example.com/")); - ResolveInfo main = pm.resolveActivity(intent, 0); - - List ris = pm.queryIntentActivities(intent, flags); - size += write(os, "Browsers=" + (ris == null ? null : ris.size()) + "\r\n"); - if (ris != null) - for (ResolveInfo ri : ris) { - CharSequence label = pm.getApplicationLabel(ri.activityInfo.applicationInfo); - - Intent serviceIntent = new Intent(); - serviceIntent.setAction(ACTION_CUSTOM_TABS_CONNECTION); - serviceIntent.setPackage(ri.activityInfo.packageName); - boolean tabs = (pm.resolveService(serviceIntent, 0) != null); - - StringBuilder sb = new StringBuilder(); - sb.append("Browser=").append(ri.activityInfo.packageName); - if (Objects.equals(main == null ? null : main.activityInfo.packageName, ri.activityInfo.packageName)) - sb.append("*"); - sb.append(" (").append(label).append(")"); - sb.append(" tabs=").append(tabs); - sb.append(" view=").append(ri.filter.hasAction(Intent.ACTION_VIEW)); - sb.append(" browsable=").append(ri.filter.hasCategory(Intent.CATEGORY_BROWSABLE)); - sb.append(" authorities=").append(ri.filter.authoritiesIterator() != null); - sb.append(" schemes="); - - boolean first = true; - Iterator schemeIter = ri.filter.schemesIterator(); - while (schemeIter.hasNext()) { - String scheme = schemeIter.next(); - if (first) - first = false; - else - sb.append(','); - sb.append(scheme); - } - - if (tabs && BuildConfig.DEBUG) - try { - boolean bindable = context.bindService(serviceIntent, new CustomTabsServiceConnection() { - @Override - public void onCustomTabsServiceConnected(@NonNull final ComponentName component, final CustomTabsClient client) { - try { - context.unbindService(this); - } catch (Throwable ex) { - Log.e(ex); - } - } - - @Override - public void onServiceDisconnected(final ComponentName component) { - // Do nothing - } - }, 0); - sb.append(" bindable=").append(bindable); - } catch (Throwable ex) { - size += write(os, ex.toString() + "\r\n"); - } - - sb.append("\r\n"); - size += write(os, sb.toString()); - } - - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); - String open_with_pkg = prefs.getString("open_with_pkg", null); - boolean open_with_tabs = prefs.getBoolean("open_with_tabs", true); - size += write(os, String.format("Selected: %s tabs=%b\r\n", - open_with_pkg, open_with_tabs)); - } catch (Throwable ex) { - size += write(os, String.format("%s\r\n", ex)); - } - size += write(os, "\r\n"); - - try { - Intent intent = new Intent(MediaStore.Audio.Media.RECORD_SOUND_ACTION); - ResolveInfo main = pm.resolveActivity(intent, 0); - List ris = pm.queryIntentActivities(intent, flags); - size += write(os, "Recorders=" + (ris == null ? null : ris.size()) + "\r\n"); - if (ris != null) - for (ResolveInfo ri : ris) { - CharSequence label = pm.getApplicationLabel(ri.activityInfo.applicationInfo); - - StringBuilder sb = new StringBuilder(); - sb.append("Recorder=").append(ri.activityInfo.packageName); - if (Objects.equals(main.activityInfo.packageName, ri.activityInfo.packageName)) - sb.append("*"); - sb.append(" (").append(label).append(")"); - - sb.append("\r\n"); - size += write(os, sb.toString()); - } - } catch (Throwable ex) { - size += write(os, String.format("%s\r\n", ex)); - } - size += write(os, "\r\n"); - - try { - List uperms = context.getContentResolver().getPersistedUriPermissions(); - size += write(os, "Persisted URIs=" + (uperms == null ? null : uperms.size()) + "\r\n"); - if (uperms != null) - for (UriPermission uperm : uperms) { - size += write(os, String.format("%s r=%b w=%b %s\r\n", - uperm.getUri().toString(), - uperm.isReadPermission(), - uperm.isWritePermission(), - new Date(uperm.getPersistedTime()))); - } - } catch (Throwable ex) { - size += write(os, String.format("%s\r\n", ex)); - } - size += write(os, "\r\n"); - - try { - PackageInfo pi = pm.getPackageInfo(BuildConfig.APPLICATION_ID, PackageManager.GET_PERMISSIONS); - for (int i = 0; i < pi.requestedPermissions.length; i++) - if (pi.requestedPermissions[i] != null && - pi.requestedPermissions[i].startsWith("android.permission.")) { - boolean granted = ((pi.requestedPermissionsFlags[i] & PackageInfo.REQUESTED_PERMISSION_GRANTED) != 0); - size += write(os, String.format("%s=%b\r\n", - pi.requestedPermissions[i].replace("android.permission.", ""), granted)); - } - } catch (Throwable ex) { - size += write(os, String.format("%s\r\n", ex)); - } - size += write(os, "\r\n"); - - for (String prop : NETWORK_PROPS) - size += write(os, prop + "=" + System.getProperty(prop) + "\r\n"); - size += write(os, "\r\n"); - - ApplicationInfo ai = context.getApplicationInfo(); - if (ai != null) - size += write(os, String.format("Source: %s\r\n public: %s\r\n", - ai.sourceDir, ai.publicSourceDir)); - size += write(os, String.format("Files: %s\r\n", context.getFilesDir())); - - size += write(os, String.format("Cache: %s\r\n external: %s\n", - context.getCacheDir(), context.getExternalCacheDir())); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) - size += write(os, String.format("Data: %s\r\n", context.getDataDir().getAbsolutePath())); - size += write(os, String.format("Database: %s\r\n", - context.getDatabasePath(DB.DB_NAME))); - - try (Cursor cursor = SQLiteDatabase.create(null).rawQuery( - "SELECT sqlite_version() AS sqlite_version", null)) { - if (cursor.moveToNext()) - size += write(os, String.format("sqlite: %s\r\n", cursor.getString(0))); - } - try { - TupleFtsStats stats = db.message().getFts(); - size += write(os, String.format("fts: %d/%d %s\r\n", stats.fts, stats.total, - Helper.humanReadableByteCount(Fts4DbHelper.size(context)))); - } catch (Throwable ex) { - size += write(os, String.format("%s\r\n", ex)); - } - - size += write(os, "\r\n"); - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - try { - DomainVerificationManager dvm = Helper.getSystemService(context, DomainVerificationManager.class); - DomainVerificationUserState userState = dvm.getDomainVerificationUserState(context.getPackageName()); - Map hostToStateMap = userState.getHostToStateMap(); - for (String key : hostToStateMap.keySet()) { - Integer stateValue = hostToStateMap.get(key); - if (stateValue == DomainVerificationUserState.DOMAIN_STATE_VERIFIED) - size += write(os, String.format("Verified: %s\r\n", key)); - else if (stateValue == DomainVerificationUserState.DOMAIN_STATE_SELECTED) - size += write(os, String.format("selected: %s\r\n", key)); - else - size += write(os, String.format("Unverified: %s (%d)\r\n", key, - stateValue == null ? -1 : stateValue)); - } - } catch (Throwable ex) { - size += write(os, String.format("%s\r\n", ex)); - } - size += write(os, "\r\n"); - } - - try { - List works = WorkManager - .getInstance(context) - .getWorkInfos(WorkQuery.fromStates( - WorkInfo.State.ENQUEUED, - WorkInfo.State.BLOCKED, - WorkInfo.State.RUNNING)) - .get(); - for (WorkInfo work : works) { - size += write(os, String.format("Work: %s\r\n", - work.toString())); - } - } catch (Throwable ex) { - size += write(os, String.format("%s\r\n", ex)); - } - - size += write(os, "\r\n"); - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) - try { - Map exts = SdkExtensions.getAllExtensionVersions(); - for (Integer ext : exts.keySet()) - size += write(os, String.format("Extension %d / %d\r\n", ext, exts.get(ext))); - if (exts.size() > 0) - size += write(os, "\r\n"); - - size += write(os, String.format("Max. pick images: %d\r\n\r\n", MediaStore.getPickImagesMaxLimit())); - } catch (Throwable ex) { - size += write(os, String.format("%s\r\n", ex)); - } - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - try { - for (FileStore store : FileSystems.getDefault().getFileStores()) - if (!store.isReadOnly() && - store.getUsableSpace() != 0 && - !"tmpfs".equals(store.type())) { - long total = store.getTotalSpace(); - long unalloc = store.getUnallocatedSpace(); - size += write(os, String.format("%s %s %s/%s\r\n", - store, - store.type(), - Helper.humanReadableByteCount(total - unalloc), - Helper.humanReadableByteCount(total))); - } - } catch (IOException ex) { - size += write(os, String.format("%s\r\n", ex)); - } - size += write(os, "\r\n"); - } - - List files = new ArrayList<>(); - try { - files.addAll(Helper.listFiles(context.getFilesDir(), MIN_FILE_SIZE)); - } catch (Throwable ex) { - size += write(os, String.format("%s\r\n", ex)); - } - try { - files.addAll(Helper.listFiles(context.getCacheDir(), MIN_FILE_SIZE)); - } catch (Throwable ex) { - size += write(os, String.format("%s\r\n", ex)); - } - - Collections.sort(files, new Comparator() { - @Override - public int compare(File f1, File f2) { - return -Long.compare(f1.length(), f2.length()); - } - }); - - for (int i = 0; i < Math.min(100, files.size()); i++) - size += write(os, String.format("%d %s %s\r\n", i + 1, - Helper.humanReadableByteCount(files.get(i).length()), - files.get(i).getAbsoluteFile())); - size += write(os, "\r\n"); - - size += write(os, String.format("Configuration: %s\r\n\r\n", - context.getResources().getConfiguration())); - - for (Provider p : Security.getProviders()) - size += write(os, String.format("%s\r\n", p)); - size += write(os, "\r\n"); - - String pgpPackage = PgpHelper.getPackageName(context); - boolean pgpInstalled = PgpHelper.isOpenKeychainInstalled(context); - size += write(os, String.format("%s=%b\r\n", pgpPackage, pgpInstalled)); - - if (pgpInstalled) - try { - PackageInfo pi = pm.getPackageInfo(pgpPackage, PackageManager.GET_PERMISSIONS); - for (int i = 0; i < pi.requestedPermissions.length; i++) { - boolean granted = ((pi.requestedPermissionsFlags[i] & PackageInfo.REQUESTED_PERMISSION_GRANTED) != 0); - size += write(os, String.format("- %s=%b\r\n", pi.requestedPermissions[i], granted)); - } - } catch (Throwable ex) { - size += write(os, String.format("%s\r\n", ex)); - } - - try { - int maxKeySize = javax.crypto.Cipher.getMaxAllowedKeyLength("AES"); - size += write(os, context.getString(R.string.title_advanced_aes_key_size, - Helper.humanReadableByteCount(maxKeySize, false))); - size += write(os, "\r\n"); - } catch (Throwable ex) { - size += write(os, String.format("%s\r\n", ex)); - } - size += write(os, "\r\n"); - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - try { - Map stats = Debug.getRuntimeStats(); - for (String key : stats.keySet()) - size += write(os, String.format("%s=%s\r\n", key, stats.get(key))); - } catch (Throwable ex) { - size += write(os, String.format("%s\r\n", ex)); - } - size += write(os, "\r\n"); - } - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - try { - // https://developer.android.com/reference/android/app/ApplicationExitInfo - ActivityManager am = Helper.getSystemService(context, ActivityManager.class); - List infos = am.getHistoricalProcessExitReasons( - context.getPackageName(), 0, 100); - for (ApplicationExitInfo info : infos) - size += write(os, String.format("%s: %s %s/%s reason=%s status=%d importance=%d\r\n", - new Date(info.getTimestamp()), info.getDescription(), - Helper.humanReadableByteCount(info.getPss() * 1024L), - Helper.humanReadableByteCount(info.getRss() * 1024L), - Helper.getExitReason(info.getReason()), info.getStatus(), info.getImportance())); - } catch (Throwable ex) { - size += write(os, String.format("%s\r\n", ex)); - } - - size += write(os, "\r\n"); - } - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) - try { - UsageStatsManager usm = Helper.getSystemService(context, UsageStatsManager.class); - UsageEvents events = usm.queryEventsForSelf(now - 12 * 3600L, now); - UsageEvents.Event event = new UsageEvents.Event(); - while (events != null && events.hasNextEvent()) { - events.getNextEvent(event); - size += write(os, String.format("%s %s %s b=%d s=%d\r\n", - new Date(event.getTimeStamp()), - Helper.getEventType(event.getEventType()), - event.getClassName(), - event.getAppStandbyBucket(), - event.getShortcutId())); - } - } catch (Throwable ex) { - size += write(os, String.format("%s\r\n", ex)); - } - - try { - List groups = pm.getAllPermissionGroups(0); - groups.add(0, null); // Ungrouped - - for (PermissionGroupInfo group : groups) { - String name = (group == null ? null : group.name); - size += write(os, String.format("\r\n%s\r\n", name == null ? "Ungrouped" : name)); - size += write(os, "----------------------------------------\r\n"); - - try { - for (PermissionInfo permission : pm.queryPermissionsByGroup(name, 0)) - size += write(os, String.format("%s\r\n", permission.name)); - } catch (Throwable ex) { - size += write(os, String.format("%s\r\n", ex)); - } - } - } catch (Throwable ex) { - size += write(os, String.format("%s\r\n", ex)); - } - } - - db.attachment().setDownloaded(attachment.id, size); - } catch (Throwable ex) { - Log.e(ex); - } - } - - private static void attachClassifierData(Context context, long id, int sequence) throws IOException, JSONException { - DB db = DB.getInstance(context); - - EntityAttachment attachment = new EntityAttachment(); - attachment.message = id; - attachment.sequence = sequence; - attachment.name = "classifier.json"; - attachment.type = "application/json"; - attachment.disposition = Part.ATTACHMENT; - attachment.size = null; - attachment.progress = 0; - attachment.id = db.attachment().insertAttachment(attachment); - - MessageClassifier.save(context); - File source = MessageClassifier.getFile(context, false); - File target = attachment.getFile(context); - Helper.copy(source, target); - - db.attachment().setDownloaded(attachment.id, target.length()); - } - - static String getTokenInfo(String password, int auth_type) throws JSONException { - AuthState authState = AuthState.jsonDeserialize(password); - Long expiration = authState.getAccessTokenExpirationTime(); - TokenResponse t = authState.getLastTokenResponse(); - Set scopeSet = (t == null ? null : t.getScopeSet()); - String[] scopes = (scopeSet == null ? new String[0] : scopeSet.toArray(new String[0])); - return String.format("%s expire=%s need=%b %s", - ServiceAuthenticator.getAuthTypeName(auth_type), - (expiration == null ? null : new Date(expiration)), - authState.getNeedsTokenRefresh(), - TextUtils.join(",", scopes)); - } - static SpannableStringBuilder getCiphers() { SpannableStringBuilder ssb = new SpannableStringBuilderEx(); @@ -3826,12 +1948,6 @@ public class Log { return ssb; } - private static int write(OutputStream os, String text) throws IOException { - byte[] bytes = text.getBytes(); - os.write(bytes); // TODO CASA system info - return bytes.length; - } - private static long getFreeMem() { Runtime rt = Runtime.getRuntime(); long used = (rt.totalMemory() - rt.freeMemory());