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