This class can be used to enable the use of HierarchyViewer inside an 48 | * application. HierarchyViewer is an Android SDK tool that can be used 49 | * to inspect and debug the user interface of running applications. For 50 | * security reasons, HierarchyViewer does not work on production builds 51 | * (for instance phones bought in store.) By using this class, you can 52 | * make HierarchyViewer work on any device. You must be very careful 53 | * however to only enable HierarchyViewer when debugging your 54 | * application.
55 | * 56 | *To use this view server, your application must require the INTERNET 57 | * permission.
58 | * 59 | *The recommended way to use this API is to register activities when 60 | * they are created, and to unregister them when they get destroyed:
61 | * 62 | *63 | * public class MyActivity extends Activity { 64 | * public void onCreate(Bundle savedInstanceState) { 65 | * super.onCreate(savedInstanceState); 66 | * // Set content view, etc. 67 | * ViewServer.get(this).addWindow(this); 68 | * } 69 | * 70 | * public void onDestroy() { 71 | * super.onDestroy(); 72 | * ViewServer.get(this).removeWindow(this); 73 | * } 74 | * 75 | * public void onResume() { 76 | * super.onResume(); 77 | * ViewServer.get(this).setFocusedWindow(this); 78 | * } 79 | * } 80 | *81 | * 82 | *
83 | * In a similar fashion, you can use this API with an InputMethodService: 84 | *
85 | * 86 | *87 | * public class MyInputMethodService extends InputMethodService { 88 | * public void onCreate() { 89 | * super.onCreate(); 90 | * View decorView = getWindow().getWindow().getDecorView(); 91 | * String name = "MyInputMethodService"; 92 | * ViewServer.get(this).addWindow(decorView, name); 93 | * } 94 | * 95 | * public void onDestroy() { 96 | * super.onDestroy(); 97 | * View decorView = getWindow().getWindow().getDecorView(); 98 | * ViewServer.get(this).removeWindow(decorView); 99 | * } 100 | * 101 | * public void onStartInput(EditorInfo attribute, boolean restarting) { 102 | * super.onStartInput(attribute, restarting); 103 | * View decorView = getWindow().getWindow().getDecorView(); 104 | * ViewServer.get(this).setFocusedWindow(decorView); 105 | * } 106 | * } 107 | *108 | */ 109 | public class ViewServer implements Runnable { 110 | /** 111 | * The default port used to start view servers. 112 | */ 113 | private static final int VIEW_SERVER_DEFAULT_PORT = 4939; 114 | private static final int VIEW_SERVER_MAX_CONNECTIONS = 10; 115 | private static final String BUILD_TYPE_USER = "user"; 116 | 117 | // Debug facility 118 | private static final String LOG_TAG = "ViewServer"; 119 | 120 | private static final String VALUE_PROTOCOL_VERSION = "4"; 121 | private static final String VALUE_SERVER_VERSION = "4"; 122 | 123 | // Protocol commands 124 | // Returns the protocol version 125 | private static final String COMMAND_PROTOCOL_VERSION = "PROTOCOL"; 126 | // Returns the server version 127 | private static final String COMMAND_SERVER_VERSION = "SERVER"; 128 | // Lists all of the available windows in the system 129 | private static final String COMMAND_WINDOW_MANAGER_LIST = "LIST"; 130 | // Keeps a connection open and notifies when the list of windows changes 131 | private static final String COMMAND_WINDOW_MANAGER_AUTOLIST = "AUTOLIST"; 132 | // Returns the focused window 133 | private static final String COMMAND_WINDOW_MANAGER_GET_FOCUS = "GET_FOCUS"; 134 | 135 | private ServerSocket mServer; 136 | private final int mPort; 137 | 138 | private Thread mThread; 139 | private ExecutorService mThreadPool; 140 | 141 | private final List
android:debuggable
158 | * flag set in its manifest, the server returned by this method will
159 | * be a dummy object that does not do anything. This allows you to use
160 | * the same code in debug and release versions of your application.
161 | *
162 | * @param context A Context used to check whether the application is
163 | * debuggable, this can be the application context
164 | */
165 | public static ViewServer get(Context context) {
166 | ApplicationInfo info = context.getApplicationInfo();
167 | if (BUILD_TYPE_USER.equals(Build.TYPE) &&
168 | (info.flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0) {
169 | if (sServer == null) {
170 | sServer = new ViewServer(ViewServer.VIEW_SERVER_DEFAULT_PORT);
171 | }
172 |
173 | if (!sServer.isRunning()) {
174 | try {
175 | sServer.start();
176 | } catch (IOException e) {
177 | Log.d(LOG_TAG, "Error:", e);
178 | }
179 | }
180 | } else {
181 | sServer = new NoopViewServer();
182 | }
183 |
184 | return sServer;
185 | }
186 |
187 | private ViewServer() {
188 | mPort = -1;
189 | }
190 |
191 | /**
192 | * Creates a new ViewServer associated with the specified window manager on the
193 | * specified local port. The server is not started by default.
194 | *
195 | * @param port The port for the server to listen to.
196 | *
197 | * @see #start()
198 | */
199 | private ViewServer(int port) {
200 | mPort = port;
201 | }
202 |
203 | /**
204 | * Starts the server.
205 | *
206 | * @return True if the server was successfully created, or false if it already exists.
207 | * @throws IOException If the server cannot be created.
208 | *
209 | * @see #stop()
210 | * @see #isRunning()
211 | */
212 | public boolean start() throws IOException {
213 | if (mThread != null) {
214 | return false;
215 | }
216 |
217 | mThread = new Thread(this, "Local View Server [port=" + mPort + "]");
218 | mThreadPool = Executors.newFixedThreadPool(VIEW_SERVER_MAX_CONNECTIONS);
219 | mThread.start();
220 |
221 | return true;
222 | }
223 |
224 | /**
225 | * Stops the server.
226 | *
227 | * @return True if the server was stopped, false if an error occurred or if the
228 | * server wasn't started.
229 | *
230 | * @see #start()
231 | * @see #isRunning()
232 | */
233 | public boolean stop() {
234 | if (mThread != null) {
235 | mThread.interrupt();
236 | if (mThreadPool != null) {
237 | try {
238 | mThreadPool.shutdownNow();
239 | } catch (SecurityException e) {
240 | Log.w(LOG_TAG, "Could not stop all view server threads");
241 | }
242 | }
243 |
244 | mThreadPool = null;
245 | mThread = null;
246 |
247 | try {
248 | mServer.close();
249 | mServer = null;
250 | return true;
251 | } catch (IOException e) {
252 | Log.w(LOG_TAG, "Could not close the view server");
253 | }
254 | }
255 |
256 | mWindowsLock.writeLock().lock();
257 | try {
258 | mWindows.clear();
259 | } finally {
260 | mWindowsLock.writeLock().unlock();
261 | }
262 |
263 | mFocusLock.writeLock().lock();
264 | try {
265 | mFocusedWindow = null;
266 | } finally {
267 | mFocusLock.writeLock().unlock();
268 | }
269 |
270 | return false;
271 | }
272 |
273 | /**
274 | * Indicates whether the server is currently running.
275 | *
276 | * @return True if the server is running, false otherwise.
277 | *
278 | * @see #start()
279 | * @see #stop()
280 | */
281 | public boolean isRunning() {
282 | return mThread != null && mThread.isAlive();
283 | }
284 |
285 | /**
286 | * Invoke this method to register a new view hierarchy.
287 | *
288 | * @param activity The activity whose view hierarchy/window to register
289 | *
290 | * @see #addWindow(View, String)
291 | * @see #removeWindow(Activity)
292 | */
293 | public void addWindow(Activity activity) {
294 | String name = activity.getTitle().toString();
295 | if (TextUtils.isEmpty(name)) {
296 | name = activity.getClass().getCanonicalName() +
297 | "/0x" + System.identityHashCode(activity);
298 | } else {
299 | name += "(" + activity.getClass().getCanonicalName() + ")";
300 | }
301 | addWindow(activity.getWindow().getDecorView(), name);
302 | }
303 |
304 | /**
305 | * Invoke this method to unregister a view hierarchy.
306 | *
307 | * @param activity The activity whose view hierarchy/window to unregister
308 | *
309 | * @see #addWindow(Activity)
310 | * @see #removeWindow(View)
311 | */
312 | public void removeWindow(Activity activity) {
313 | removeWindow(activity.getWindow().getDecorView());
314 | }
315 |
316 | /**
317 | * Invoke this method to register a new view hierarchy.
318 | *
319 | * @param view A view that belongs to the view hierarchy/window to register
320 | * @name name The name of the view hierarchy/window to register
321 | *
322 | * @see #removeWindow(View)
323 | */
324 | public void addWindow(View view, String name) {
325 | mWindowsLock.writeLock().lock();
326 | try {
327 | mWindows.put(view.getRootView(), name);
328 | } finally {
329 | mWindowsLock.writeLock().unlock();
330 | }
331 | fireWindowsChangedEvent();
332 | }
333 |
334 | /**
335 | * Invoke this method to unregister a view hierarchy.
336 | *
337 | * @param view A view that belongs to the view hierarchy/window to unregister
338 | *
339 | * @see #addWindow(View, String)
340 | */
341 | public void removeWindow(View view) {
342 | View rootView;
343 | mWindowsLock.writeLock().lock();
344 | try {
345 | rootView = view.getRootView();
346 | mWindows.remove(rootView);
347 | } finally {
348 | mWindowsLock.writeLock().unlock();
349 | }
350 | mFocusLock.writeLock().lock();
351 | try {
352 | if (mFocusedWindow == rootView) {
353 | mFocusedWindow = null;
354 | }
355 | } finally {
356 | mFocusLock.writeLock().unlock();
357 | }
358 | fireWindowsChangedEvent();
359 | }
360 |
361 | /**
362 | * Invoke this method to change the currently focused window.
363 | *
364 | * @param activity The activity whose view hierarchy/window hasfocus,
365 | * or null to remove focus
366 | */
367 | public void setFocusedWindow(Activity activity) {
368 | setFocusedWindow(activity.getWindow().getDecorView());
369 | }
370 |
371 | /**
372 | * Invoke this method to change the currently focused window.
373 | *
374 | * @param view A view that belongs to the view hierarchy/window that has focus,
375 | * or null to remove focus
376 | */
377 | public void setFocusedWindow(View view) {
378 | mFocusLock.writeLock().lock();
379 | try {
380 | mFocusedWindow = view == null ? null : view.getRootView();
381 | } finally {
382 | mFocusLock.writeLock().unlock();
383 | }
384 | fireFocusChangedEvent();
385 | }
386 |
387 | /**
388 | * Main server loop.
389 | */
390 | public void run() {
391 | try {
392 | mServer = new ServerSocket(mPort, VIEW_SERVER_MAX_CONNECTIONS, InetAddress.getLocalHost());
393 | } catch (Exception e) {
394 | Log.w(LOG_TAG, "Starting ServerSocket error: ", e);
395 | }
396 |
397 | while (mServer != null && Thread.currentThread() == mThread) {
398 | // Any uncaught exception will crash the system process
399 | try {
400 | Socket client = mServer.accept();
401 | if (mThreadPool != null) {
402 | mThreadPool.submit(new ViewServerWorker(client));
403 | } else {
404 | try {
405 | client.close();
406 | } catch (IOException e) {
407 | e.printStackTrace();
408 | }
409 | }
410 | } catch (Exception e) {
411 | Log.w(LOG_TAG, "Connection error: ", e);
412 | }
413 | }
414 | }
415 |
416 | private static boolean writeValue(Socket client, String value) {
417 | boolean result;
418 | BufferedWriter out = null;
419 | try {
420 | OutputStream clientStream = client.getOutputStream();
421 | out = new BufferedWriter(new OutputStreamWriter(clientStream), 8 * 1024);
422 | out.write(value);
423 | out.write("\n");
424 | out.flush();
425 | result = true;
426 | } catch (Exception e) {
427 | result = false;
428 | } finally {
429 | if (out != null) {
430 | try {
431 | out.close();
432 | } catch (IOException e) {
433 | result = false;
434 | }
435 | }
436 | }
437 | return result;
438 | }
439 |
440 | private void fireWindowsChangedEvent() {
441 | for (WindowListener listener : mListeners) {
442 | listener.windowsChanged();
443 | }
444 | }
445 |
446 | private void fireFocusChangedEvent() {
447 | for (WindowListener listener : mListeners) {
448 | listener.focusChanged();
449 | }
450 | }
451 |
452 | private void addWindowListener(WindowListener listener) {
453 | if (!mListeners.contains(listener)) {
454 | mListeners.add(listener);
455 | }
456 | }
457 |
458 | private void removeWindowListener(WindowListener listener) {
459 | mListeners.remove(listener);
460 | }
461 |
462 | private interface WindowListener {
463 | void windowsChanged();
464 | void focusChanged();
465 | }
466 |
467 | private static class UncloseableOutputStream extends OutputStream {
468 | private final OutputStream mStream;
469 |
470 | UncloseableOutputStream(OutputStream stream) {
471 | mStream = stream;
472 | }
473 |
474 | public void close() throws IOException {
475 | // Don't close the stream
476 | }
477 |
478 | public boolean equals(Object o) {
479 | return mStream.equals(o);
480 | }
481 |
482 | public void flush() throws IOException {
483 | mStream.flush();
484 | }
485 |
486 | public int hashCode() {
487 | return mStream.hashCode();
488 | }
489 |
490 | public String toString() {
491 | return mStream.toString();
492 | }
493 |
494 | public void write(byte[] buffer, int offset, int count)
495 | throws IOException {
496 | mStream.write(buffer, offset, count);
497 | }
498 |
499 | public void write(byte[] buffer) throws IOException {
500 | mStream.write(buffer);
501 | }
502 |
503 | public void write(int oneByte) throws IOException {
504 | mStream.write(oneByte);
505 | }
506 | }
507 |
508 | private static class NoopViewServer extends ViewServer {
509 | private NoopViewServer() {
510 | }
511 |
512 | @Override
513 | public boolean start() throws IOException {
514 | return false;
515 | }
516 |
517 | @Override
518 | public boolean stop() {
519 | return false;
520 | }
521 |
522 | @Override
523 | public boolean isRunning() {
524 | return false;
525 | }
526 |
527 | @Override
528 | public void addWindow(Activity activity) {
529 | }
530 |
531 | @Override
532 | public void removeWindow(Activity activity) {
533 | }
534 |
535 | @Override
536 | public void addWindow(View view, String name) {
537 | }
538 |
539 | @Override
540 | public void removeWindow(View view) {
541 | }
542 |
543 | @Override
544 | public void setFocusedWindow(Activity activity) {
545 | }
546 |
547 | @Override
548 | public void setFocusedWindow(View view) {
549 | }
550 |
551 | @Override
552 | public void run() {
553 | }
554 | }
555 |
556 | private class ViewServerWorker implements Runnable, WindowListener {
557 | private Socket mClient;
558 | private boolean mNeedWindowListUpdate;
559 | private boolean mNeedFocusedWindowUpdate;
560 |
561 | private final Object[] mLock = new Object[0];
562 |
563 | public ViewServerWorker(Socket client) {
564 | mClient = client;
565 | mNeedWindowListUpdate = false;
566 | mNeedFocusedWindowUpdate = false;
567 | }
568 |
569 | public void run() {
570 | BufferedReader in = null;
571 | try {
572 | in = new BufferedReader(new InputStreamReader(mClient.getInputStream()), 1024);
573 |
574 | final String request = in.readLine();
575 |
576 | String command;
577 | String parameters;
578 |
579 | int index = request.indexOf(' ');
580 | if (index == -1) {
581 | command = request;
582 | parameters = "";
583 | } else {
584 | command = request.substring(0, index);
585 | parameters = request.substring(index + 1);
586 | }
587 |
588 | boolean result;
589 | if (COMMAND_PROTOCOL_VERSION.equalsIgnoreCase(command)) {
590 | result = writeValue(mClient, VALUE_PROTOCOL_VERSION);
591 | } else if (COMMAND_SERVER_VERSION.equalsIgnoreCase(command)) {
592 | result = writeValue(mClient, VALUE_SERVER_VERSION);
593 | } else if (COMMAND_WINDOW_MANAGER_LIST.equalsIgnoreCase(command)) {
594 | result = listWindows(mClient);
595 | } else if (COMMAND_WINDOW_MANAGER_GET_FOCUS.equalsIgnoreCase(command)) {
596 | result = getFocusedWindow(mClient);
597 | } else if (COMMAND_WINDOW_MANAGER_AUTOLIST.equalsIgnoreCase(command)) {
598 | result = windowManagerAutolistLoop();
599 | } else {
600 | result = windowCommand(mClient, command, parameters);
601 | }
602 |
603 | if (!result) {
604 | Log.w(LOG_TAG, "An error occurred with the command: " + command);
605 | }
606 | } catch(IOException e) {
607 | Log.w(LOG_TAG, "Connection error: ", e);
608 | } finally {
609 | if (in != null) {
610 | try {
611 | in.close();
612 |
613 | } catch (IOException e) {
614 | e.printStackTrace();
615 | }
616 | }
617 | if (mClient != null) {
618 | try {
619 | mClient.close();
620 | } catch (IOException e) {
621 | e.printStackTrace();
622 | }
623 | }
624 | }
625 | }
626 |
627 | private boolean windowCommand(Socket client, String command, String parameters) {
628 | boolean success = true;
629 | BufferedWriter out = null;
630 |
631 | try {
632 | // Find the hash code of the window
633 | int index = parameters.indexOf(' ');
634 | if (index == -1) {
635 | index = parameters.length();
636 | }
637 | final String code = parameters.substring(0, index);
638 | int hashCode = (int) Long.parseLong(code, 16);
639 |
640 | // Extract the command's parameter after the window description
641 | if (index < parameters.length()) {
642 | parameters = parameters.substring(index + 1);
643 | } else {
644 | parameters = "";
645 | }
646 |
647 | final View window = findWindow(hashCode);
648 | if (window == null) {
649 | return false;
650 | }
651 |
652 | // call stuff
653 | final Method dispatch = ViewDebug.class.getDeclaredMethod("dispatchCommand",
654 | View.class, String.class, String.class, OutputStream.class);
655 | dispatch.setAccessible(true);
656 | dispatch.invoke(null, window, command, parameters,
657 | new UncloseableOutputStream(client.getOutputStream()));
658 |
659 | if (!client.isOutputShutdown()) {
660 | out = new BufferedWriter(new OutputStreamWriter(client.getOutputStream()));
661 | out.write("DONE\n");
662 | out.flush();
663 | }
664 |
665 | } catch (Exception e) {
666 | Log.w(LOG_TAG, "Could not send command " + command +
667 | " with parameters " + parameters, e);
668 | success = false;
669 | } finally {
670 | if (out != null) {
671 | try {
672 | out.close();
673 | } catch (IOException e) {
674 | success = false;
675 | }
676 | }
677 | }
678 |
679 | return success;
680 | }
681 |
682 | private View findWindow(int hashCode) {
683 | if (hashCode == -1) {
684 | View window = null;
685 | mWindowsLock.readLock().lock();
686 | try {
687 | window = mFocusedWindow;
688 | } finally {
689 | mWindowsLock.readLock().unlock();
690 | }
691 | return window;
692 | }
693 |
694 |
695 | mWindowsLock.readLock().lock();
696 | try {
697 | for (Entry