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