1 /* 2 * Copyright (C) 2015 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.messaging.util; 18 19 import android.app.Activity; 20 import android.app.AlertDialog; 21 import android.app.FragmentManager; 22 import android.app.FragmentTransaction; 23 import android.content.Context; 24 import android.content.DialogInterface; 25 import android.media.MediaPlayer; 26 import android.os.Environment; 27 import android.telephony.SmsMessage; 28 import android.text.TextUtils; 29 import android.widget.ArrayAdapter; 30 31 import com.android.messaging.R; 32 import com.android.messaging.datamodel.SyncManager; 33 import com.android.messaging.datamodel.action.DumpDatabaseAction; 34 import com.android.messaging.datamodel.action.LogTelephonyDatabaseAction; 35 import com.android.messaging.sms.MmsUtils; 36 import com.android.messaging.ui.UIIntents; 37 import com.android.messaging.ui.debug.DebugSmsMmsFromDumpFileDialogFragment; 38 import com.google.common.io.ByteStreams; 39 40 import java.io.BufferedInputStream; 41 import java.io.DataInputStream; 42 import java.io.DataOutputStream; 43 import java.io.File; 44 import java.io.FileInputStream; 45 import java.io.FileNotFoundException; 46 import java.io.FileOutputStream; 47 import java.io.FilenameFilter; 48 import java.io.IOException; 49 import java.io.StreamCorruptedException; 50 51 public class DebugUtils { 52 private static final String TAG = "bugle.util.DebugUtils"; 53 54 private static boolean sDebugNoise; 55 private static boolean sDebugClassZeroSms; 56 private static MediaPlayer [] sMediaPlayer; 57 private static final Object sLock = new Object(); 58 59 public static final int DEBUG_SOUND_SERVER_REQUEST = 0; 60 public static final int DEBUG_SOUND_DB_OP = 1; 61 62 public static void maybePlayDebugNoise(final Context context, final int sound) { 63 if (sDebugNoise) { 64 synchronized (sLock) { 65 try { 66 if (sMediaPlayer == null) { 67 sMediaPlayer = new MediaPlayer[2]; 68 sMediaPlayer[DEBUG_SOUND_SERVER_REQUEST] = 69 MediaPlayer.create(context, R.raw.server_request_debug); 70 sMediaPlayer[DEBUG_SOUND_DB_OP] = 71 MediaPlayer.create(context, R.raw.db_op_debug); 72 sMediaPlayer[DEBUG_SOUND_DB_OP].setVolume(1.0F, 1.0F); 73 sMediaPlayer[DEBUG_SOUND_SERVER_REQUEST].setVolume(0.3F, 0.3F); 74 } 75 if (sMediaPlayer[sound] != null) { 76 sMediaPlayer[sound].start(); 77 } 78 } catch (final IllegalArgumentException e) { 79 LogUtil.e(TAG, "MediaPlayer exception", e); 80 } catch (final SecurityException e) { 81 LogUtil.e(TAG, "MediaPlayer exception", e); 82 } catch (final IllegalStateException e) { 83 LogUtil.e(TAG, "MediaPlayer exception", e); 84 } 85 } 86 } 87 } 88 89 public static boolean isDebugEnabled() { 90 return BugleGservices.get().getBoolean(BugleGservicesKeys.ENABLE_DEBUGGING_FEATURES, 91 BugleGservicesKeys.ENABLE_DEBUGGING_FEATURES_DEFAULT); 92 } 93 94 public abstract static class DebugAction { 95 String mTitle; 96 public DebugAction(final String title) { 97 mTitle = title; 98 } 99 100 @Override 101 public String toString() { 102 return mTitle; 103 } 104 105 public abstract void run(); 106 } 107 108 public static void showDebugOptions(final Activity host) { 109 final AlertDialog.Builder builder = new AlertDialog.Builder(host); 110 111 final ArrayAdapter<DebugAction> arrayAdapter = new ArrayAdapter<DebugAction>( 112 host, android.R.layout.simple_list_item_1); 113 114 arrayAdapter.add(new DebugAction("Dump Database") { 115 @Override 116 public void run() { 117 DumpDatabaseAction.dumpDatabase(); 118 } 119 }); 120 121 arrayAdapter.add(new DebugAction("Log Telephony Data") { 122 @Override 123 public void run() { 124 LogTelephonyDatabaseAction.dumpDatabase(); 125 } 126 }); 127 128 arrayAdapter.add(new DebugAction("Toggle Noise") { 129 @Override 130 public void run() { 131 sDebugNoise = !sDebugNoise; 132 } 133 }); 134 135 arrayAdapter.add(new DebugAction("Force sync SMS") { 136 @Override 137 public void run() { 138 final BuglePrefs prefs = BuglePrefs.getApplicationPrefs(); 139 prefs.putLong(BuglePrefsKeys.LAST_FULL_SYNC_TIME, -1); 140 SyncManager.forceSync(); 141 } 142 }); 143 144 arrayAdapter.add(new DebugAction("Sync SMS") { 145 @Override 146 public void run() { 147 SyncManager.sync(); 148 } 149 }); 150 151 arrayAdapter.add(new DebugAction("Load SMS/MMS from dump file") { 152 @Override 153 public void run() { 154 new DebugSmsMmsDumpTask(host, 155 DebugSmsMmsFromDumpFileDialogFragment.ACTION_LOAD).executeOnThreadPool(); 156 } 157 }); 158 159 arrayAdapter.add(new DebugAction("Email SMS/MMS dump file") { 160 @Override 161 public void run() { 162 new DebugSmsMmsDumpTask(host, 163 DebugSmsMmsFromDumpFileDialogFragment.ACTION_EMAIL).executeOnThreadPool(); 164 } 165 }); 166 167 arrayAdapter.add(new DebugAction("MMS Config...") { 168 @Override 169 public void run() { 170 UIIntents.get().launchDebugMmsConfigActivity(host); 171 } 172 }); 173 174 arrayAdapter.add(new DebugAction(sDebugClassZeroSms ? "Turn off Class 0 sms test" : 175 "Turn on Class Zero test") { 176 @Override 177 public void run() { 178 sDebugClassZeroSms = !sDebugClassZeroSms; 179 } 180 }); 181 182 builder.setAdapter(arrayAdapter, 183 new android.content.DialogInterface.OnClickListener() { 184 @Override 185 public void onClick(final DialogInterface arg0, final int pos) { 186 arrayAdapter.getItem(pos).run(); 187 } 188 }); 189 190 builder.create().show(); 191 } 192 193 /** 194 * Task to list all the dump files and perform an action on it 195 */ 196 private static class DebugSmsMmsDumpTask extends SafeAsyncTask<Void, Void, String[]> { 197 private final String mAction; 198 private final Activity mHost; 199 200 public DebugSmsMmsDumpTask(final Activity host, final String action) { 201 mHost = host; 202 mAction = action; 203 } 204 205 @Override 206 protected void onPostExecute(final String[] result) { 207 if (result == null || result.length < 1) { 208 return; 209 } 210 final FragmentManager fragmentManager = mHost.getFragmentManager(); 211 final FragmentTransaction ft = fragmentManager.beginTransaction(); 212 final DebugSmsMmsFromDumpFileDialogFragment dialog = 213 DebugSmsMmsFromDumpFileDialogFragment.newInstance(result, mAction); 214 dialog.show(fragmentManager, ""/*tag*/); 215 } 216 217 @Override 218 protected String[] doInBackgroundTimed(final Void... params) { 219 final File dir = DebugUtils.getDebugFilesDir(); 220 return dir.list(new FilenameFilter() { 221 @Override 222 public boolean accept(final File dir, final String filename) { 223 return filename != null 224 && ((mAction == DebugSmsMmsFromDumpFileDialogFragment.ACTION_EMAIL 225 && filename.equals(DumpDatabaseAction.DUMP_NAME)) 226 || filename.startsWith(MmsUtils.MMS_DUMP_PREFIX) 227 || filename.startsWith(MmsUtils.SMS_DUMP_PREFIX)); 228 } 229 }); 230 } 231 } 232 233 /** 234 * Dump the received raw SMS data into a file on external storage 235 * 236 * @param id The ID to use as part of the dump file name 237 * @param messages The raw SMS data 238 */ 239 public static void dumpSms(final long id, final android.telephony.SmsMessage[] messages, 240 final String format) { 241 try { 242 final String dumpFileName = MmsUtils.SMS_DUMP_PREFIX + Long.toString(id); 243 final File dumpFile = DebugUtils.getDebugFile(dumpFileName, true); 244 if (dumpFile != null) { 245 final FileOutputStream fos = new FileOutputStream(dumpFile); 246 final DataOutputStream dos = new DataOutputStream(fos); 247 try { 248 final int chars = (TextUtils.isEmpty(format) ? 0 : format.length()); 249 dos.writeInt(chars); 250 if (chars > 0) { 251 dos.writeUTF(format); 252 } 253 dos.writeInt(messages.length); 254 for (final android.telephony.SmsMessage message : messages) { 255 final byte[] pdu = message.getPdu(); 256 dos.writeInt(pdu.length); 257 dos.write(pdu, 0, pdu.length); 258 } 259 dos.flush(); 260 } finally { 261 dos.close(); 262 ensureReadable(dumpFile); 263 } 264 } 265 } catch (final IOException e) { 266 LogUtil.e(LogUtil.BUGLE_TAG, "dumpSms: " + e, e); 267 } 268 } 269 270 /** 271 * Load MMS/SMS from the dump file 272 */ 273 public static SmsMessage[] retreiveSmsFromDumpFile(final String dumpFileName) { 274 SmsMessage[] messages = null; 275 final File inputFile = DebugUtils.getDebugFile(dumpFileName, false); 276 if (inputFile != null) { 277 FileInputStream fis = null; 278 DataInputStream dis = null; 279 try { 280 fis = new FileInputStream(inputFile); 281 dis = new DataInputStream(fis); 282 283 // SMS dump 284 final int chars = dis.readInt(); 285 if (chars > 0) { 286 final String format = dis.readUTF(); 287 } 288 final int count = dis.readInt(); 289 final SmsMessage[] messagesTemp = new SmsMessage[count]; 290 for (int i = 0; i < count; i++) { 291 final int length = dis.readInt(); 292 final byte[] pdu = new byte[length]; 293 dis.read(pdu, 0, length); 294 messagesTemp[i] = SmsMessage.createFromPdu(pdu); 295 } 296 messages = messagesTemp; 297 } catch (final FileNotFoundException e) { 298 // Nothing to do 299 } catch (final StreamCorruptedException e) { 300 // Nothing to do 301 } catch (final IOException e) { 302 // Nothing to do 303 } finally { 304 if (dis != null) { 305 try { 306 dis.close(); 307 } catch (final IOException e) { 308 // Nothing to do 309 } 310 } 311 } 312 } 313 return messages; 314 } 315 316 public static File getDebugFile(final String fileName, final boolean create) { 317 final File dir = getDebugFilesDir(); 318 final File file = new File(dir, fileName); 319 if (create && file.exists()) { 320 file.delete(); 321 } 322 return file; 323 } 324 325 public static File getDebugFilesDir() { 326 final File dir = Environment.getExternalStorageDirectory(); 327 return dir; 328 } 329 330 /** 331 * Load MMS/SMS from the dump file 332 */ 333 public static byte[] receiveFromDumpFile(final String dumpFileName) { 334 byte[] data = null; 335 try { 336 final File inputFile = getDebugFile(dumpFileName, false); 337 if (inputFile != null) { 338 final FileInputStream fis = new FileInputStream(inputFile); 339 final BufferedInputStream bis = new BufferedInputStream(fis); 340 try { 341 // dump file 342 data = ByteStreams.toByteArray(bis); 343 if (data == null || data.length < 1) { 344 LogUtil.e(LogUtil.BUGLE_TAG, "receiveFromDumpFile: empty data"); 345 } 346 } finally { 347 bis.close(); 348 } 349 } 350 } catch (final IOException e) { 351 LogUtil.e(LogUtil.BUGLE_TAG, "receiveFromDumpFile: " + e, e); 352 } 353 return data; 354 } 355 356 public static void ensureReadable(final File file) { 357 if (file.exists()){ 358 file.setReadable(true, false); 359 } 360 } 361 362 /** 363 * Logs the name of the method that is currently executing, e.g. "MyActivity.onCreate". This is 364 * useful for surgically adding logs for tracing execution while debugging. 365 * <p> 366 * NOTE: This method retrieves the current thread's stack trace, which adds runtime overhead. 367 * However, this method is only executed on eng builds if DEBUG logs are loggable. 368 */ 369 public static void logCurrentMethod(String tag) { 370 if (!LogUtil.isLoggable(tag, LogUtil.DEBUG)) { 371 return; 372 } 373 StackTraceElement caller = getCaller(1); 374 if (caller == null) { 375 return; 376 } 377 String className = caller.getClassName(); 378 // Strip off the package name 379 int lastDot = className.lastIndexOf('.'); 380 if (lastDot > -1) { 381 className = className.substring(lastDot + 1); 382 } 383 LogUtil.d(tag, className + "." + caller.getMethodName()); 384 } 385 386 /** 387 * Returns info about the calling method. The {@code depth} parameter controls how far back to 388 * go. For example, if foo() calls bar(), and bar() calls getCaller(0), it returns info about 389 * bar(). If bar() instead called getCaller(1), it would return info about foo(). And so on. 390 * <p> 391 * NOTE: This method retrieves the current thread's stack trace, which adds runtime overhead. 392 * It should only be used in production where necessary to gather context about an error or 393 * unexpected event (e.g. the {@link Assert} class uses it). 394 * 395 * @return stack frame information for the caller (if found); otherwise {@code null}. 396 */ 397 public static StackTraceElement getCaller(int depth) { 398 // If the signature of this method is changed, proguard.flags must be updated! 399 if (depth < 0) { 400 throw new IllegalArgumentException("depth cannot be negative"); 401 } 402 StackTraceElement[] trace = Thread.currentThread().getStackTrace(); 403 if (trace == null || trace.length < (depth + 2)) { 404 return null; 405 } 406 // The stack trace includes some methods we don't care about (e.g. this method). 407 // Walk down until we find this method, and then back up to the caller we're looking for. 408 for (int i = 0; i < trace.length - 1; i++) { 409 String methodName = trace[i].getMethodName(); 410 if ("getCaller".equals(methodName)) { 411 return trace[i + depth + 1]; 412 } 413 } 414 // Never found ourself in the stack?! 415 return null; 416 } 417 418 /** 419 * Returns a boolean indicating whether ClassZero debugging is enabled. If enabled, any received 420 * sms is treated as if it were a class zero message and displayed by the ClassZeroActivity. 421 */ 422 public static boolean debugClassZeroSmsEnabled() { 423 return sDebugClassZeroSms; 424 } 425 } 426