/* * Copyright 2013 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package me.barrasso.android.volume; import android.app.ActivityManager; import android.content.ActivityNotFoundException; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.graphics.Point; import android.media.AudioManager; import android.net.Uri; import android.os.Build; import android.preference.PreferenceManager; import android.provider.Settings; import android.text.TextUtils; import android.util.DisplayMetrics; import android.util.Log; import android.util.SparseArray; import android.view.Display; import android.view.WindowManager; // import com.crashlytics.android.Crashlytics; import com.levelup.logutils.FLog; import com.levelup.logutils.LogCollecting; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.File; import java.io.FileOutputStream; import java.io.FileReader; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.text.SimpleDateFormat; import java.util.Arrays; import java.util.Calendar; import java.util.Date; import java.util.HashSet; import java.util.Set; import java.util.TimeZone; import me.barrasso.android.volume.media.conditions.RingerNotificationLink; import me.barrasso.android.volume.media.conditions.SystemVolume; import me.barrasso.android.volume.utils.AudioHelper; import me.barrasso.android.volume.utils.Constants; import me.barrasso.android.volume.utils.Utils; import me.barrasso.android.volume.utils.VolumeManager; /** * Helper methods that make logging more consistent throughout the app. */ public class LogUtils { private static final String TAG = makeLogTag(LogUtils.class); private static final boolean SILENT = true; //!Accountant.FREE_FOR_ALL; private static final String SUPPORT_EMAIL = ""; private static final boolean CRASHLYTICS = false; private static final boolean FILE_LOGGER = true; private static final String LOG_PREFIX = "volume_"; private static final int LOG_PREFIX_LENGTH = LOG_PREFIX.length(); private static final int MAX_LOG_TAG_LENGTH = 23; private static Set<String> ERRORS = new HashSet<String>(); private LogUtils() { } public static String makeLogTag(String str) { if (str.length() > MAX_LOG_TAG_LENGTH - LOG_PREFIX_LENGTH) { return LOG_PREFIX + str.substring(0, MAX_LOG_TAG_LENGTH - LOG_PREFIX_LENGTH - 1); } return LOG_PREFIX + str; } /** * WARNING: Don't use this when obfuscating class names with Proguard! */ public static String makeLogTag(Class cls) { return makeLogTag(cls.getSimpleName()); } public static void LOGD(final String tag, String message) { //noinspection PointlessBooleanExpression,ConstantConditions if (!SILENT && (BuildConfig.DEBUG || Log.isLoggable(tag, Log.DEBUG))) { Log.d(tag, message); } if (FILE_LOGGER) FLog.d(tag, message); } public static void LOGD(final String tag, String message, Throwable cause) { //noinspection PointlessBooleanExpression,ConstantConditions if (!SILENT && (BuildConfig.DEBUG || Log.isLoggable(tag, Log.DEBUG))) { Log.d(tag, message, cause); } if (FILE_LOGGER) FLog.d(tag, message, cause); } public static void LOGV(final String tag, String message) { //noinspection PointlessBooleanExpression,ConstantConditions if (!SILENT && (BuildConfig.DEBUG && Log.isLoggable(tag, Log.VERBOSE))) { Log.v(tag, message); } if (FILE_LOGGER) FLog.v(tag, message); } public static void LOGV(final String tag, String message, Throwable cause) { //noinspection PointlessBooleanExpression,ConstantConditions if (!SILENT && (BuildConfig.DEBUG && Log.isLoggable(tag, Log.VERBOSE))) { Log.v(tag, message, cause); } if (FILE_LOGGER) FLog.v(tag, message, cause); } public static void LOGI(final String tag, String message) { if (!SILENT) Log.i(tag, message); if (FILE_LOGGER) FLog.i(tag, message); // if (CRASHLYTICS) Crashlytics.log(Log.INFO, tag, message); } public static void LOGI(final String tag, String message, Throwable cause) { Log.i(tag, message, cause); if (FILE_LOGGER) FLog.i(tag, message, cause); /*if (CRASHLYTICS) { Crashlytics.log(Log.INFO, tag, message); Crashlytics.logException(cause); }*/ } public static void LOGW(final String tag, String message) { if (!SILENT) Log.w(tag, message); if (FILE_LOGGER) FLog.w(tag, message); // if (CRASHLYTICS) Crashlytics.log(Log.WARN, tag, message); } public static void LOGW(final String tag, String message, Throwable cause) { Log.w(tag, message, cause); if (FILE_LOGGER) FLog.w(tag, message, cause); /*if (CRASHLYTICS) { Crashlytics.log(Log.WARN, tag, message); Crashlytics.logException(cause); }*/ } public static void LOGE(final String tag, String message) { Log.e(tag, message); if (FILE_LOGGER) FLog.e(tag, message); ERRORS.add(tag + ": " + message); // if (CRASHLYTICS) Crashlytics.log(Log.ERROR, tag, message); } public static void LOGE(final String tag, String message, Throwable cause) { Log.e(tag, message, cause); if (FILE_LOGGER) FLog.e(tag, message); ERRORS.add(tag + ": " + message); /* if (CRASHLYTICS) { Crashlytics.log(Log.ERROR, tag, message); Crashlytics.logException(cause); }*/ } /** * @return A log of the contents of a {@link android.util.SparseArray}, * including recursive mapping of SparseArray values. */ public static String logSparseArray(SparseArray<?> sparseArray) { StringBuilder log = new StringBuilder(); log.append('['); for(int i = 0, e = sparseArray.size(); i < e; i++) { int key = sparseArray.keyAt(i); Object obj = sparseArray.get(key); String value = (obj instanceof SparseArray) ? logSparseArray((SparseArray) obj) : String .valueOf(obj); log.append(key).append('=').append(value); if (i < (e - 1)) log.append(','); } log.append(']'); return log.toString(); } public static void sendDebugLogAsync(final Context context) { FLog.collectlogs(context, new LogCollecting() { @Override public void onLogCollected(File path, String mimeType) { LOGI(TAG, "onLogCollected(" + mimeType + ')'); StringBuilder text = new StringBuilder(); if (null != path) { try { BufferedReader br = new BufferedReader(new FileReader(path)); String line; while ((line = br.readLine()) != null) { text.append(line); text.append('\n'); } } catch (IOException e) { text = new StringBuilder(); text.append(e.getMessage()); } } else { text.append(mimeType); // reason? } sendDebugLog(context, text.toString()); } @Override public void onEmptyLogCollected() { onLogCollected(null, null); } @Override public void onLogCollectingError(String reason) { onLogCollected(null, reason); } }); } /** * Only for use with debug versions of the app! */ public static void sendDebugLog(Context context) { sendDebugLog(context, null); } /** @see {@link #sendDebugLog(Context)} */ public static void sendDebugLog(Context context, String extra) { //noinspection PointlessBooleanExpression,ConstantConditions StringBuilder log = new StringBuilder(); // Append device build fingerprint, timestamp, installer, version info, enabled services, etc. PackageManager pm = context.getPackageManager(); AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); String packageName = context.getPackageName(); String versionName; int code = 0; try { PackageInfo info = pm.getPackageInfo(packageName, 0); versionName = info.versionName; code = info.versionCode; } catch (PackageManager.NameNotFoundException e) { versionName = "??"; } log.append("App version:\n").append(versionName).append(" : ").append(code).append("\n"); SimpleDateFormat dateFormat = new SimpleDateFormat("KK:mm:ss a"); log.append("Device fingerprint:\n").append(Build.FINGERPRINT).append("\n"); log.append("Timestamp: ").append(dateFormat.format(Calendar.getInstance().getTime())).append("\n"); log.append("Build: ").append(Utils.lastBuildTimestamp(context)).append("\n"); log.append("Installer: ").append(Utils.getInstaller(context).getPackageName()).append("\n"); log.append("HTC?: ").append(AudioHelper.isHTC(context)).append("\n"); // CHECK: what app is the current media receiver. String receiverName = Settings.System.getString( context.getContentResolver(), Constants.getMediaButtonReceiver()); if (!TextUtils.isEmpty(receiverName)) { ComponentName receiverComponent = ComponentName.unflattenFromString(receiverName); log.append(Constants.getMediaButtonReceiver()).append(": ").append(receiverComponent.getPackageName()).append("\n"); } // CHECK: what notification and Accessibility services are activated. String notifKey = Constants.getEnabledNotificationListeners(); String enabledNotifs = Settings.Secure.getString(context.getContentResolver(), notifKey); String enabledAccess = Settings.Secure.getString(context.getContentResolver(), Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES); log.append(Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES).append(": ").append(enabledAccess).append("\n"); log.append(notifKey).append(": ").append(enabledNotifs).append("\n"); // CHECK: how the device handles notif-ringer link and other predicates. RingerNotificationLink linkChecker = new RingerNotificationLink(); SystemVolume sysChecked = new SystemVolume(); log.append(linkChecker.getClass().getSimpleName()).append(": ").append(linkChecker.apply(audioManager)).append("\n"); log.append(sysChecked.getClass().getSimpleName()).append(": ").append(sysChecked.apply(audioManager)).append("\n"); // Append map of all preferences log.append("Preferences:\n\n").append(Utils.getPreferencesString( PreferenceManager.getDefaultSharedPreferences(context))).append("\n\n"); // Append any errors the app experienced if (ERRORS.size() > 0) { log.append("Errors:\n\n"); for (String error : ERRORS) { log.append(error).append("\n"); } log.append("\n\n"); ERRORS.clear(); } // Append extra logs from FileLogger if (!TextUtils.isEmpty(extra)) { log.append("FileLogger:\n\n").append(extra).append("\n\n"); } // Append all system volume levels and ringer mode VolumeManager manager = new VolumeManager(audioManager); log.append("VolumeManager:\n\n").append(manager.toString()).append("\n"); log.append("Ringer mode: ").append(audioManager.getRingerMode()).append("\n"); log.append("Media receiver: ").append(Utils.getPackageNames( Utils.getMediaReceivers(context.getPackageManager()))).append("\n\n"); // Log information about the device screen/ resolution WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); Display display = wm.getDefaultDisplay(); DisplayMetrics dm = new DisplayMetrics(); Point size = new Point(); display.getSize(size); display.getMetrics(dm); log.append("Display width: ").append(size.x).append("px\n"); log.append("Display height: ").append(size.y).append("px\n"); log.append("Density: ").append(dm.densityDpi).append("dpi\n"); log.append("Density: ").append(dm.density).append("\n\n"); // Log available/ total memory info ActivityManager.MemoryInfo mi = new ActivityManager.MemoryInfo(); ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); activityManager.getMemoryInfo(mi); log.append("Memory total: ").append(mi.totalMem / 1048576L).append("mb\n"); log.append("Memory available: ").append(mi.availMem / 1048576L).append("mb\n"); log.append("Memory threshold: ").append(mi.threshold / 1048576L).append("mb\n"); log.append("Memory low?: ").append(mi.lowMemory); try { // Write everything to a file File logsDir = context.getCacheDir(); if (logsDir == null) { throw new IOException("Cache directory inaccessible"); } logsDir = new File(logsDir, "logs"); deleteRecursive(logsDir); logsDir.mkdirs(); SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd_HHmm"); sdf.setTimeZone(TimeZone.getTimeZone("UTC")); String fileName = "Noyze_log_" + sdf.format(new Date()) + ".txt"; File logFile = new File(logsDir, fileName); BufferedWriter writer = new BufferedWriter(new OutputStreamWriter( new FileOutputStream(logFile))); writer.write(log.toString()); writer.close(); logFile.setWritable(true, false); // Send the file Intent sendIntent = new Intent(Intent.ACTION_SENDTO) .setData(Uri.parse("mailto:" + SUPPORT_EMAIL)) .putExtra(Intent.EXTRA_SUBJECT, "Noyze debug log") .putExtra(Intent.EXTRA_STREAM, Uri.parse( "content://" + LogAttachmentProvider.AUTHORITY + "/" + fileName)); // Make sure there's an app for that! if (null != sendIntent.resolveActivity(context.getPackageManager())) { context.startActivity(Intent.createChooser(sendIntent, context.getString(R.string.send_logs_chooser_title))); } } catch (IOException e) { LOGE(TAG, "Error accessing or sending app's logs.", e); } catch (ActivityNotFoundException nfe) { LOGE(TAG, "Error launching an email app to mail the app's logs."); } } private static void deleteRecursive(File file) { if (file != null) { File[] children = file.listFiles(); if (children != null && children.length > 0) { for (File child : children) { deleteRecursive(child); } } else { file.delete(); } } } }