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 String format = null; 295 final int chars = dis.readInt(); 296 if (chars > 0) { 297 format = dis.readUTF(); 298 } 299 final int count = dis.readInt(); 300 final SmsMessage[] messagesTemp = new SmsMessage[count]; 301 for (int i = 0; i < count; i++) { 302 final int length = dis.readInt(); 303 final byte[] pdu = new byte[length]; 304 dis.read(pdu, 0, length); 305 messagesTemp[i] = 306 OsUtil.isAtLeastM() 307 ? SmsMessage.createFromPdu(pdu, format) 308 : SmsMessage.createFromPdu(pdu); 309 } 310 messages = messagesTemp; 311 } catch (final FileNotFoundException e) { 312 // Nothing to do 313 } catch (final StreamCorruptedException e) { 314 // Nothing to do 315 } catch (final IOException e) { 316 // Nothing to do 317 } finally { 318 if (dis != null) { 319 try { 320 dis.close(); 321 } catch (final IOException e) { 322 // Nothing to do 323 } 324 } 325 } 326 } 327 return messages; 328 } 329 330 public static File getDebugFile(final String fileName, final boolean create) { 331 final File dir = getDebugFilesDir(); 332 final File file = new File(dir, fileName); 333 if (create && file.exists()) { 334 file.delete(); 335 } 336 return file; 337 } 338 339 public static File getDebugFilesDir() { 340 final File dir = Environment.getExternalStorageDirectory(); 341 return dir; 342 } 343 344 /** 345 * Load MMS/SMS from the dump file 346 */ 347 public static byte[] receiveFromDumpFile(final String dumpFileName) { 348 byte[] data = null; 349 try { 350 final File inputFile = getDebugFile(dumpFileName, false); 351 if (inputFile != null) { 352 final FileInputStream fis = new FileInputStream(inputFile); 353 final BufferedInputStream bis = new BufferedInputStream(fis); 354 try { 355 // dump file 356 data = ByteStreams.toByteArray(bis); 357 if (data == null || data.length < 1) { 358 LogUtil.e(LogUtil.BUGLE_TAG, "receiveFromDumpFile: empty data"); 359 } 360 } finally { 361 bis.close(); 362 } 363 } 364 } catch (final IOException e) { 365 LogUtil.e(LogUtil.BUGLE_TAG, "receiveFromDumpFile: " + e, e); 366 } 367 return data; 368 } 369 370 public static void ensureReadable(final File file) { 371 if (file.exists()){ 372 file.setReadable(true, false); 373 } 374 } 375 376 /** 377 * Logs the name of the method that is currently executing, e.g. "MyActivity.onCreate". This is 378 * useful for surgically adding logs for tracing execution while debugging. 379 * <p> 380 * NOTE: This method retrieves the current thread's stack trace, which adds runtime overhead. 381 * However, this method is only executed on eng builds if DEBUG logs are loggable. 382 */ 383 public static void logCurrentMethod(String tag) { 384 if (!LogUtil.isLoggable(tag, LogUtil.DEBUG)) { 385 return; 386 } 387 StackTraceElement caller = getCaller(1); 388 if (caller == null) { 389 return; 390 } 391 String className = caller.getClassName(); 392 // Strip off the package name 393 int lastDot = className.lastIndexOf('.'); 394 if (lastDot > -1) { 395 className = className.substring(lastDot + 1); 396 } 397 LogUtil.d(tag, className + "." + caller.getMethodName()); 398 } 399 400 /** 401 * Returns info about the calling method. The {@code depth} parameter controls how far back to 402 * go. For example, if foo() calls bar(), and bar() calls getCaller(0), it returns info about 403 * bar(). If bar() instead called getCaller(1), it would return info about foo(). And so on. 404 * <p> 405 * NOTE: This method retrieves the current thread's stack trace, which adds runtime overhead. 406 * It should only be used in production where necessary to gather context about an error or 407 * unexpected event (e.g. the {@link Assert} class uses it). 408 * 409 * @return stack frame information for the caller (if found); otherwise {@code null}. 410 */ 411 public static StackTraceElement getCaller(int depth) { 412 // If the signature of this method is changed, proguard.flags must be updated! 413 if (depth < 0) { 414 throw new IllegalArgumentException("depth cannot be negative"); 415 } 416 StackTraceElement[] trace = Thread.currentThread().getStackTrace(); 417 if (trace == null || trace.length < (depth + 2)) { 418 return null; 419 } 420 // The stack trace includes some methods we don't care about (e.g. this method). 421 // Walk down until we find this method, and then back up to the caller we're looking for. 422 for (int i = 0; i < trace.length - 1; i++) { 423 String methodName = trace[i].getMethodName(); 424 if ("getCaller".equals(methodName)) { 425 return trace[i + depth + 1]; 426 } 427 } 428 // Never found ourself in the stack?! 429 return null; 430 } 431 432 /** 433 * Returns a boolean indicating whether ClassZero debugging is enabled. If enabled, any received 434 * sms is treated as if it were a class zero message and displayed by the ClassZeroActivity. 435 */ 436 public static boolean debugClassZeroSmsEnabled() { 437 return sDebugClassZeroSms; 438 } 439 440 /** Shares a ringtone file via file URI. */ 441 private static void shareFileUri() { 442 final String packageName = "com.android.messaging"; 443 final String fileName = "/system/media/audio/ringtones/Andromeda.ogg"; 444 445 Intent intent = new Intent(Intent.ACTION_SEND); 446 intent.setPackage(packageName); 447 intent.putExtra(Intent.EXTRA_STREAM, Uri.parse("file://" + fileName)); 448 intent.setType("image/*"); 449 Factory.get().getApplicationContext().startActivity(intent); 450 } 451 } 452