diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml index bf3982d9a71d4..25346acd16004 100644 --- a/packages/Shell/AndroidManifest.xml +++ b/packages/Shell/AndroidManifest.xml @@ -144,6 +144,7 @@ android:name=".BugreportReceiver" android:permission="android.permission.DUMP"> + diff --git a/packages/Shell/res/values/strings.xml b/packages/Shell/res/values/strings.xml index 4469d387853c5..cff36f73f74d5 100644 --- a/packages/Shell/res/values/strings.xml +++ b/packages/Shell/res/values/strings.xml @@ -17,6 +17,8 @@ Shell + + Bug report in progress Bug report captured diff --git a/packages/Shell/src/com/android/shell/BugreportProgressService.java b/packages/Shell/src/com/android/shell/BugreportProgressService.java index a2030ef68817b..d0e91d22f87af 100644 --- a/packages/Shell/src/com/android/shell/BugreportProgressService.java +++ b/packages/Shell/src/com/android/shell/BugreportProgressService.java @@ -21,11 +21,16 @@ import static com.android.shell.BugreportPrefs.getWarningState; import java.io.BufferedOutputStream; import java.io.File; +import java.io.FileDescriptor; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; +import java.io.PrintWriter; +import java.text.NumberFormat; +import java.text.SimpleDateFormat; import java.util.ArrayList; +import java.util.Date; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; @@ -45,26 +50,102 @@ import android.content.Intent; import android.content.res.Configuration; import android.net.Uri; import android.os.AsyncTask; +import android.os.Handler; +import android.os.HandlerThread; import android.os.IBinder; +import android.os.Looper; +import android.os.Message; +import android.os.Parcelable; +import android.os.Process; import android.os.SystemProperties; import android.support.v4.content.FileProvider; +import android.text.format.DateUtils; import android.util.Log; import android.util.Patterns; +import android.util.SparseArray; import android.widget.Toast; +/** + * Service used to keep progress of bug reports processes ({@code dumpstate}). + *

+ * The workflow is: + *

    + *
  1. When {@code dumpstate} starts, it sends a {@code BUGREPORT_STARTED} with its pid and the + * estimated total effort. + *
  2. {@link BugreportReceiver} receives the intent and delegates it to this service. + *
  3. Upon start, this service: + *
      + *
    1. Issues a system notification so user can watch the progresss (which is 0% initially). + *
    2. Polls the {@link SystemProperties} for updates on the {@code dumpstate} progress. + *
    3. If the progress changed, it updates the system notification. + *
    + *
  4. As {@code dumpstate} progresses, it updates the system property. + *
  5. When {@code dumpstate} finishes, it sends a {@code BUGREPORT_FINISHED} intent. + *
  6. {@link BugreportReceiver} receives the intent and delegates it to this service, which in + * turn: + *
      + *
    1. Updates the system notification so user can share the bug report. + *
    2. Stops monitoring that {@code dumpstate} process. + *
    3. Stops itself if it doesn't have any process left to monitor. + *
    + *
+ */ public class BugreportProgressService extends Service { private static final String TAG = "Shell"; + private static final boolean DEBUG = false; private static final String AUTHORITY = "com.android.shell"; + static final String INTENT_BUGREPORT_STARTED = "android.intent.action.BUGREPORT_STARTED"; + static final String INTENT_BUGREPORT_FINISHED = "android.intent.action.BUGREPORT_FINISHED"; + static final String EXTRA_BUGREPORT = "android.intent.extra.BUGREPORT"; static final String EXTRA_SCREENSHOT = "android.intent.extra.SCREENSHOT"; + static final String EXTRA_PID = "android.intent.extra.PID"; + static final String EXTRA_MAX = "android.intent.extra.MAX"; + static final String EXTRA_NAME = "android.intent.extra.NAME"; + static final String EXTRA_ORIGINAL_INTENT = "android.intent.extra.ORIGINAL_INTENT"; + + private static final int MSG_SERVICE_COMMAND = 1; + private static final int MSG_POLL = 2; + + /** Polling frequency, in milliseconds. */ + private static final long POLLING_FREQUENCY = 500; + + /** How long (in ms) a dumpstate process will be monitored if it didn't show progress. */ + private static final long INACTIVITY_TIMEOUT = 3 * DateUtils.MINUTE_IN_MILLIS; + + /** System property used for monitoring progress. */ + private static final String PROGRESS_PROPERTY_TEMPLATE = "dumpstate.%d.progress"; + + /** Managed dumpstate processes (keyed by pid) */ + private final SparseArray mProcesses = new SparseArray<>(); + + private Looper mServiceLooper; + private ServiceHandler mServiceHandler; + + @Override + public void onCreate() { + HandlerThread thread = new HandlerThread("BugreportProgressServiceThread", + Process.THREAD_PRIORITY_BACKGROUND); + thread.start(); + + mServiceLooper = thread.getLooper(); + mServiceHandler = new ServiceHandler(mServiceLooper); + } @Override public int onStartCommand(Intent intent, int flags, int startId) { if (intent != null) { - onBugreportFinished(intent); + // Handle it in a separate thread. + Message msg = mServiceHandler.obtainMessage(); + msg.what = MSG_SERVICE_COMMAND; + msg.obj = intent; + mServiceHandler.sendMessage(msg); } + + // If service is killed it cannot be recreated because it would not know which + // dumpstate PIDs it would have to watch. return START_NOT_STICKY; } @@ -73,26 +154,249 @@ public class BugreportProgressService extends Service { return null; } - private void onBugreportFinished(Intent intent) { - final Context context = getApplicationContext(); - final Configuration conf = context.getResources().getConfiguration(); - final File bugreportFile = getFileExtra(intent, EXTRA_BUGREPORT); - final File screenshotFile = getFileExtra(intent, EXTRA_SCREENSHOT); + @Override + public void onDestroy() { + mServiceLooper.quit(); + super.onDestroy(); + } - if ((conf.uiMode & Configuration.UI_MODE_TYPE_MASK) != Configuration.UI_MODE_TYPE_WATCH) { - triggerLocalNotification(context, bugreportFile, screenshotFile); + @Override + protected void dump(FileDescriptor fd, PrintWriter writer, String[] args) { + writer.printf("Monitored dumpstate processes: \n"); + synchronized (mProcesses) { + for (int i = 0; i < mProcesses.size(); i++) { + writer.printf("\t%s\n", mProcesses.valueAt(i)); + } + } + } + + private final class ServiceHandler extends Handler { + public ServiceHandler(Looper looper) { + super(looper); + pollProgress(); + } + + @Override + public void handleMessage(Message msg) { + if (msg.what == MSG_POLL) { + pollProgress(); + return; + } + + if (msg.what != MSG_SERVICE_COMMAND) { + // Sanity check. + Log.e(TAG, "Invalid message type: " + msg.what); + return; + } + + // At this point it's handling onStartCommand(), whose intent contains the extras + // originally received by BugreportReceiver. + if (!(msg.obj instanceof Intent)) { + // Sanity check. + Log.e(TAG, "Internal error: invalid msg.obj: " + msg.obj); + return; + } + final Parcelable parcel = ((Intent) msg.obj).getParcelableExtra(EXTRA_ORIGINAL_INTENT); + if (!(parcel instanceof Intent)) { + // Sanity check. + Log.e(TAG, "Internal error: msg.obj is missing extra " + EXTRA_ORIGINAL_INTENT); + return; + } + + final Intent intent = (Intent) parcel; + final String action = intent.getAction(); + int pid = intent.getIntExtra(EXTRA_PID, 0); + int max = intent.getIntExtra(EXTRA_MAX, -1); + String name = intent.getStringExtra(EXTRA_NAME); + + if (DEBUG) Log.v(TAG, "action: " + action + ", name: " + name + ", pid: " + pid + + ", max: "+ max); + switch (action) { + case INTENT_BUGREPORT_STARTED: + if (!startProgress(name, pid, max)) { + stopSelfWhenDone(); + return; + } + break; + case INTENT_BUGREPORT_FINISHED: + if (pid == -1) { + // Shouldn't happen, unless BUGREPORT_FINISHED is received from a legacy, + // out-of-sync dumpstate process. + Log.w(TAG, "Missing " + EXTRA_PID + " on intent " + intent); + } + stopProgress(pid, intent); + break; + default: + Log.w(TAG, "Unsupported intent: " + action); + } + return; + + } + + /** + * Creates the {@link BugreportInfo} for a process and issue a system notification to + * indicate its progress. + * + * @return whether it succeeded or not. + */ + private boolean startProgress(String name, int pid, int max) { + if (name == null) { + Log.w(TAG, "Missing " + EXTRA_NAME + " on start intent"); + name = "N/A"; + } + if (pid == -1) { + Log.e(TAG, "Missing " + EXTRA_PID + " on start intent"); + return false; + } + if (max <= 0) { + Log.e(TAG, "Invalid value for extra " + EXTRA_MAX + ": " + max); + return false; + } + + final BugreportInfo info = new BugreportInfo(pid, name, max); + synchronized (mProcesses) { + if (mProcesses.indexOfKey(pid) >= 0) { + Log.w(TAG, "PID " + pid + " already watched"); + } else { + mProcesses.put(info.pid, info); + } + } + updateProgress(info); + return true; + } + + /** + * Updates the system notification for a given bug report. + */ + private void updateProgress(BugreportInfo info) { + if (info.max <= 0 || info.progress < 0 || info.name == null) { + Log.e(TAG, "Invalid progress values for " + info); + return; + } + + final Context context = getApplicationContext(); + final NumberFormat nf = NumberFormat.getPercentInstance(); + nf.setMinimumFractionDigits(2); + nf.setMaximumFractionDigits(2); + final String percentText = nf.format((double) info.progress / info.max); + + final String title = context.getString(R.string.bugreport_in_progress_title); + final Notification notification = new Notification.Builder(context) + .setSmallIcon(com.android.internal.R.drawable.stat_sys_adb) + .setContentTitle(title) + .setTicker(title) + .setContentText(info.name) + .setContentInfo(percentText) + .setProgress(info.max, info.progress, false) + // TODO: .setOngoing(true) once it has a CANCEL action + .setLocalOnly(true) + .setColor(context.getColor( + com.android.internal.R.color.system_notification_accent_color)) + .build(); + + NotificationManager.from(context).notify(TAG, info.pid, notification); + } + + /** + * Finalizes the progress on a given process and sends the finished intent. + */ + private void stopProgress(int pid, Intent intent) { + synchronized (mProcesses) { + if (mProcesses.indexOfKey(pid) < 0) { + Log.w(TAG, "PID not watched: " + pid); + } else { + mProcesses.remove(pid); + } + stopSelfWhenDone(); + } + if (DEBUG) Log.v(TAG, "stopProgress(" + pid + "): cancel notification"); + NotificationManager.from(getApplicationContext()).cancel(TAG, pid); + if (intent != null) { + // Bug report finished fine: send a new, different notification. + if (DEBUG) Log.v(TAG, "stopProgress(" + pid + "): finish bug report"); + onBugreportFinished(pid, intent); + } + } + + /** + * Poll {@link SystemProperties} to get the progress on each monitored process. + */ + private void pollProgress() { + synchronized (mProcesses) { + if (mProcesses.size() == 0) { + Log.d(TAG, "No process to poll progress."); + } + for (int i = 0; i < mProcesses.size(); i++) { + int pid = mProcesses.keyAt(i); + String property = String.format(PROGRESS_PROPERTY_TEMPLATE, pid); + int progress; + try { + progress = SystemProperties.getInt(property, 0); + } catch (IllegalArgumentException e) { + Log.v(TAG, "Could not read system property " + property, e); + continue; + } + if (progress == 0) { + Log.v(TAG, "System property " + property + " is not set yet"); + continue; + } + + BugreportInfo info = mProcesses.valueAt(i); + + if (progress != info.progress) { + if (DEBUG) Log.v(TAG, "Updating progress for PID " + pid + " from " + + info.progress + " to " + progress); + info.progress = progress; + info.lastUpdate = System.currentTimeMillis(); + updateProgress(info); + } else { + long inactiveTime = System.currentTimeMillis() - info.lastUpdate; + if (inactiveTime >= INACTIVITY_TIMEOUT) { + Log.w(TAG, "No progress update for process " + pid + " since " + + info.getFormattedLastUpdate()); + stopProgress(info.pid, null); + } + } + } + // Keep polling... + sendEmptyMessageDelayed(MSG_POLL, POLLING_FREQUENCY); + } + } + + /** + * Finishes the service when it's not monitoring any more processes. + */ + private void stopSelfWhenDone() { + synchronized (mProcesses) { + if (mProcesses.size() > 0) { + if (DEBUG) Log.v(TAG, "Staying alive, waiting for pids " + mProcesses); + return; + } + Log.v(TAG, "No more pids to handle, shutting down"); + stopSelf(); + } + } + + private void onBugreportFinished(int pid, Intent intent) { + final Context context = getApplicationContext(); + final Configuration conf = context.getResources().getConfiguration(); + final File bugreportFile = getFileExtra(intent, EXTRA_BUGREPORT); + final File screenshotFile = getFileExtra(intent, EXTRA_SCREENSHOT); + + if ((conf.uiMode & Configuration.UI_MODE_TYPE_MASK) != Configuration.UI_MODE_TYPE_WATCH) { + triggerLocalNotification(context, pid, bugreportFile, screenshotFile); + } } - stopSelf(); } /** - * Responsible for triggering a notification that allows the user to start a - * "share" intent with the bug report. On watches we have other methods to allow the user to - * start this intent (usually by triggering it on another connected device); we don't need to - * display the notification in this case. + * Responsible for triggering a notification that allows the user to start a "share" intent with + * the bug report. On watches we have other methods to allow the user to start this intent + * (usually by triggering it on another connected device); we don't need to display the + * notification in this case. */ - private static void triggerLocalNotification(final Context context, final File bugreportFile, - final File screenshotFile) { + private static void triggerLocalNotification(final Context context, final int pid, + final File bugreportFile, final File screenshotFile) { if (!bugreportFile.exists() || !bugreportFile.canRead()) { Log.e(TAG, "Could not read bugreport file " + bugreportFile); Toast.makeText(context, context.getString(R.string.bugreport_unreadable_text), @@ -103,10 +407,10 @@ public class BugreportProgressService extends Service { boolean isPlainText = bugreportFile.getName().toLowerCase().endsWith(".txt"); if (!isPlainText) { // Already zipped, send it right away. - sendBugreportNotification(context, bugreportFile, screenshotFile); + sendBugreportNotification(context, pid, bugreportFile, screenshotFile); } else { // Asynchronously zip the file first, then send it. - sendZippedBugreportNotification(context, bugreportFile, screenshotFile); + sendZippedBugreportNotification(context, pid, bugreportFile, screenshotFile); } } @@ -155,7 +459,7 @@ public class BugreportProgressService extends Service { /** * Sends a bugreport notitication. */ - private static void sendBugreportNotification(Context context, File bugreportFile, + private static void sendBugreportNotification(Context context, int pid, File bugreportFile, File screenshotFile) { // Files are kept on private storage, so turn into Uris that we can // grant temporary permissions for. @@ -173,10 +477,11 @@ public class BugreportProgressService extends Service { } notifIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + final String title = context.getString(R.string.bugreport_finished_title); final Notification.Builder builder = new Notification.Builder(context) .setSmallIcon(com.android.internal.R.drawable.stat_sys_adb) - .setContentTitle(context.getString(R.string.bugreport_finished_title)) - .setTicker(context.getString(R.string.bugreport_finished_title)) + .setContentTitle(title) + .setTicker(title) .setContentText(context.getString(R.string.bugreport_finished_text)) .setContentIntent(PendingIntent.getActivity( context, 0, notifIntent, PendingIntent.FLAG_CANCEL_CURRENT)) @@ -185,19 +490,19 @@ public class BugreportProgressService extends Service { .setColor(context.getColor( com.android.internal.R.color.system_notification_accent_color)); - NotificationManager.from(context).notify(TAG, 0, builder.build()); + NotificationManager.from(context).notify(TAG, pid, builder.build()); } /** * Sends a zipped bugreport notification. */ private static void sendZippedBugreportNotification(final Context context, - final File bugreportFile, final File screenshotFile) { + final int pid, final File bugreportFile, final File screenshotFile) { new AsyncTask() { @Override protected Void doInBackground(Void... params) { File zippedFile = zipBugreport(bugreportFile); - sendBugreportNotification(context, zippedFile, screenshotFile); + sendBugreportNotification(context, pid, zippedFile, screenshotFile); return null; } }.execute(); @@ -213,8 +518,8 @@ public class BugreportProgressService extends Service { Log.v(TAG, "zipping " + bugreportPath + " as " + zippedPath); File bugreportZippedFile = new File(zippedPath); try (InputStream is = new FileInputStream(bugreportFile); - ZipOutputStream zos = new ZipOutputStream( - new BufferedOutputStream(new FileOutputStream(bugreportZippedFile)))) { + ZipOutputStream zos = new ZipOutputStream( + new BufferedOutputStream(new FileOutputStream(bugreportZippedFile)))) { ZipEntry entry = new ZipEntry(bugreportFile.getName()); entry.setTime(bugreportFile.lastModified()); zos.putNextEntry(entry); @@ -230,8 +535,8 @@ public class BugreportProgressService extends Service { } return bugreportZippedFile; } catch (IOException e) { - Log.e(TAG, "exception zipping file " + zippedPath, e); - return bugreportFile; // Return original. + Log.e(TAG, "exception zipping file " + zippedPath, e); + return bugreportFile; // Return original. } } @@ -281,4 +586,55 @@ public class BugreportProgressService extends Service { return null; } } + + /** + * Information about a bug report process while its in progress. + */ + private static final class BugreportInfo { + /** + * {@code pid} of the {@code dumpstate} process generating the bug report. + */ + final int pid; + + /** + * Name of the bug report, will be used to rename the final files. + *

+ * Initial value is the bug report filename reported by {@code dumpstate}, but user can + * change it later to a more meaningful name. + */ + final String name; + + /** + * Maximum progress of the bug report generation. + */ + final int max; + + /** + * Current progress of the bug report generation. + */ + int progress; + + /** + * Time of the last progress update. + */ + long lastUpdate = System.currentTimeMillis(); + + BugreportInfo(int pid, String name, int max) { + this.pid = pid; + this.name = name; + this.max = max; + } + + String getFormattedLastUpdate() { + return SimpleDateFormat.getDateTimeInstance().format(new Date(lastUpdate)); + } + + @Override + public String toString() { + final float percent = ((float) progress * 100 / max); + return String.format("Progress for %s (pid=%d): %d/%d (%2.2f%%) Last update: %s", name, + pid, progress, max, percent, + getFormattedLastUpdate()); + } + } } diff --git a/packages/Shell/src/com/android/shell/BugreportReceiver.java b/packages/Shell/src/com/android/shell/BugreportReceiver.java index f1da14d57c24f..5133162a1ec9a 100644 --- a/packages/Shell/src/com/android/shell/BugreportReceiver.java +++ b/packages/Shell/src/com/android/shell/BugreportReceiver.java @@ -17,6 +17,8 @@ package com.android.shell; import static com.android.shell.BugreportProgressService.EXTRA_BUGREPORT; +import static com.android.shell.BugreportProgressService.EXTRA_ORIGINAL_INTENT; +import static com.android.shell.BugreportProgressService.INTENT_BUGREPORT_FINISHED; import static com.android.shell.BugreportProgressService.getFileExtra; import java.io.File; @@ -50,13 +52,16 @@ public class BugreportReceiver extends BroadcastReceiver { // Clean up older bugreports in background cleanupOldFiles(intent); - // Delegate to service. + // Delegate intent handling to service. Intent serviceIntent = new Intent(context, BugreportProgressService.class); - serviceIntent.putExtras(intent.getExtras()); + serviceIntent.putExtra(EXTRA_ORIGINAL_INTENT, intent); context.startService(serviceIntent); } private void cleanupOldFiles(Intent intent) { + if (!INTENT_BUGREPORT_FINISHED.equals(intent.getAction())) { + return; + } final File bugreportFile = getFileExtra(intent, EXTRA_BUGREPORT); final PendingResult result = goAsync(); new AsyncTask() { diff --git a/packages/Shell/tests/src/com/android/shell/BugreportReceiverTest.java b/packages/Shell/tests/src/com/android/shell/BugreportReceiverTest.java index 1bdd9ddc874d6..33c4ef1ffbd54 100644 --- a/packages/Shell/tests/src/com/android/shell/BugreportReceiverTest.java +++ b/packages/Shell/tests/src/com/android/shell/BugreportReceiverTest.java @@ -19,7 +19,12 @@ package com.android.shell; import static android.test.MoreAsserts.assertContainsRegex; import static com.android.shell.ActionSendMultipleConsumerActivity.UI_NAME; import static com.android.shell.BugreportProgressService.EXTRA_BUGREPORT; +import static com.android.shell.BugreportProgressService.EXTRA_MAX; +import static com.android.shell.BugreportProgressService.EXTRA_NAME; +import static com.android.shell.BugreportProgressService.EXTRA_PID; import static com.android.shell.BugreportProgressService.EXTRA_SCREENSHOT; +import static com.android.shell.BugreportProgressService.INTENT_BUGREPORT_FINISHED; +import static com.android.shell.BugreportProgressService.INTENT_BUGREPORT_STARTED; import java.io.BufferedOutputStream; import java.io.BufferedWriter; @@ -96,6 +101,33 @@ public class BugreportReceiverTest extends InstrumentationTestCase { cancelExistingNotifications(); } + public void testFullWorkflow() throws Exception { + final String name = "BUG, Y U NO REPORT?"; + // TODO: call method to remove property instead + SystemProperties.set("dumpstate.42.progress", "-1"); + + Intent intent = new Intent(INTENT_BUGREPORT_STARTED); + intent.putExtra(EXTRA_PID, 42); + intent.putExtra(EXTRA_NAME, name); + intent.putExtra(EXTRA_MAX, 1000); + mContext.sendBroadcast(intent); + + assertProgressNotification(name, "0.00%"); + + SystemProperties.set("dumpstate.42.progress", "108"); + assertProgressNotification(name, "10.80%"); + + SystemProperties.set("dumpstate.42.progress", "500"); + assertProgressNotification(name, "50.00%"); + + createTextFile(PLAIN_TEXT_PATH, BUGREPORT_CONTENT); + createTextFile(SCREENSHOT_PATH, SCREENSHOT_CONTENT); + Bundle extras = sendBugreportFinishedIntent(42, PLAIN_TEXT_PATH, SCREENSHOT_PATH); + assertActionSendMultiple(extras, BUGREPORT_CONTENT, SCREENSHOT_CONTENT); + + // TODO: assert service is down + } + public void testBugreportFinished_plainBugreportAndScreenshot() throws Exception { createTextFile(PLAIN_TEXT_PATH, BUGREPORT_CONTENT); createTextFile(SCREENSHOT_PATH, SCREENSHOT_CONTENT); @@ -131,13 +163,32 @@ public class BugreportReceiverTest extends InstrumentationTestCase { } } + private void assertProgressNotification(String name, String percent) { + // TODO: it current looks for 3 distinct objects, without taking advantage of their + // relationship. + String title = mContext.getString(R.string.bugreport_in_progress_title); + Log.v(TAG, "Looking for progress notification title: '" + title+ "'"); + mUiBot.getNotification(title); + Log.v(TAG, "Looking for progress notification details: '" + name + "-" + percent + "'"); + mUiBot.getObject(name); + mUiBot.getObject(percent); + } + /** * Sends a "bugreport finished" intent and waits for the result. * * @return extras sent to the bugreport finished consumer. */ private Bundle sendBugreportFinishedIntent(String bugreportPath, String screenshotPath) { - Intent intent = new Intent("android.intent.action.BUGREPORT_FINISHED"); + return sendBugreportFinishedIntent(null, bugreportPath, screenshotPath); + } + + private Bundle sendBugreportFinishedIntent(Integer pid, String bugreportPath, + String screenshotPath) { + Intent intent = new Intent(INTENT_BUGREPORT_FINISHED); + if (pid != null) { + intent.putExtra(EXTRA_PID, pid); + } if (bugreportPath != null) { intent.putExtra(EXTRA_BUGREPORT, bugreportPath); } diff --git a/packages/Shell/tests/src/com/android/shell/UiBot.java b/packages/Shell/tests/src/com/android/shell/UiBot.java index f5dd31c6fdc02..fa1714efcd41d 100644 --- a/packages/Shell/tests/src/com/android/shell/UiBot.java +++ b/packages/Shell/tests/src/com/android/shell/UiBot.java @@ -42,32 +42,49 @@ final class UiBot { } /** - * Opens the system notification and clicks a given notification. + * Opens the system notification and gets a given notification. * * @param text Notificaton's text as displayed by the UI. + * @return notification object. */ - public void clickOnNotification(String text) { + public UiObject getNotification(String text) { boolean opened = mDevice.openNotification(); Log.v(TAG, "openNotification(): " + opened); boolean gotIt = mDevice.wait(Until.hasObject(By.pkg(SYSTEMUI_PACKAGED)), mTimeout); assertTrue("could not get system ui (" + SYSTEMUI_PACKAGED + ")", gotIt); - gotIt = mDevice.wait(Until.hasObject(By.text(text)), mTimeout); - assertTrue("object with text '(" + text + "') not visible yet", gotIt); - - UiObject notification = getVisibleObject(text); + return getObject(text); + } + /** + * Opens the system notification and clicks a given notification. + * + * @param text Notificaton's text as displayed by the UI. + */ + public void clickOnNotification(String text) { + UiObject notification = getNotification(text); click(notification, "bug report notification"); } /** - * Gets an object which is guaranteed to be present in the current UI.\ + * Gets an object that might not yet be available in current UI. + * + * @param text Object's text as displayed by the UI. + */ + public UiObject getObject(String text) { + boolean gotIt = mDevice.wait(Until.hasObject(By.text(text)), mTimeout); + assertTrue("object with text '(" + text + "') not visible yet", gotIt); + return getVisibleObject(text); + } + + /** + * Gets an object which is guaranteed to be present in the current UI. * * @param text Object's text as displayed by the UI. */ public UiObject getVisibleObject(String text) { UiObject uiObject = mDevice.findObject(new UiSelector().text(text)); - assertTrue("could not find object with text '(" + text + "')", uiObject.exists()); + assertTrue("could not find object with text '" + text + "'", uiObject.exists()); return uiObject; }