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 Listandroid: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 | * @see WindowManagerService#startViewServer(int)
212 | */
213 | public boolean start() throws IOException {
214 | if (mThread != null) {
215 | return false;
216 | }
217 |
218 | mThread = new Thread(this, "Local View Server [port=" + mPort + "]");
219 | mThreadPool = Executors.newFixedThreadPool(VIEW_SERVER_MAX_CONNECTIONS);
220 | mThread.start();
221 |
222 | return true;
223 | }
224 |
225 | /**
226 | * Stops the server.
227 | *
228 | * @return True if the server was stopped, false if an error occurred or if the
229 | * server wasn't started.
230 | *
231 | * @see #start()
232 | * @see #isRunning()
233 | * @see WindowManagerService#stopViewServer()
234 | */
235 | public boolean stop() {
236 | if (mThread != null) {
237 | mThread.interrupt();
238 | if (mThreadPool != null) {
239 | try {
240 | mThreadPool.shutdownNow();
241 | } catch (SecurityException e) {
242 | Log.w(LOG_TAG, "Could not stop all view server threads");
243 | }
244 | }
245 |
246 | mThreadPool = null;
247 | mThread = null;
248 |
249 | try {
250 | mServer.close();
251 | mServer = null;
252 | return true;
253 | } catch (IOException e) {
254 | Log.w(LOG_TAG, "Could not close the view server");
255 | }
256 | }
257 |
258 | mWindowsLock.writeLock().lock();
259 | try {
260 | mWindows.clear();
261 | } finally {
262 | mWindowsLock.writeLock().unlock();
263 | }
264 |
265 | mFocusLock.writeLock().lock();
266 | try {
267 | mFocusedWindow = null;
268 | } finally {
269 | mFocusLock.writeLock().unlock();
270 | }
271 |
272 | return false;
273 | }
274 |
275 | /**
276 | * Indicates whether the server is currently running.
277 | *
278 | * @return True if the server is running, false otherwise.
279 | *
280 | * @see #start()
281 | * @see #stop()
282 | * @see WindowManagerService#isViewServerRunning()
283 | */
284 | public boolean isRunning() {
285 | return mThread != null && mThread.isAlive();
286 | }
287 |
288 | /**
289 | * Invoke this method to register a new view hierarchy.
290 | *
291 | * @param activity The activity whose view hierarchy/window to register
292 | *
293 | * @see #addWindow(View, String)
294 | * @see #removeWindow(Activity)
295 | */
296 | public void addWindow(Activity activity) {
297 | String name = activity.getTitle().toString();
298 | if (TextUtils.isEmpty(name)) {
299 | name = activity.getClass().getCanonicalName() +
300 | "/0x" + System.identityHashCode(activity);
301 | } else {
302 | name += "(" + activity.getClass().getCanonicalName() + ")";
303 | }
304 | addWindow(activity.getWindow().getDecorView(), name);
305 | }
306 |
307 | /**
308 | * Invoke this method to unregister a view hierarchy.
309 | *
310 | * @param activity The activity whose view hierarchy/window to unregister
311 | *
312 | * @see #addWindow(Activity)
313 | * @see #removeWindow(View)
314 | */
315 | public void removeWindow(Activity activity) {
316 | removeWindow(activity.getWindow().getDecorView());
317 | }
318 |
319 | /**
320 | * Invoke this method to register a new view hierarchy.
321 | *
322 | * @param view A view that belongs to the view hierarchy/window to register
323 | * @name name The name of the view hierarchy/window to register
324 | *
325 | * @see #removeWindow(View)
326 | */
327 | public void addWindow(View view, String name) {
328 | mWindowsLock.writeLock().lock();
329 | try {
330 | mWindows.put(view.getRootView(), name);
331 | } finally {
332 | mWindowsLock.writeLock().unlock();
333 | }
334 | fireWindowsChangedEvent();
335 | }
336 |
337 | /**
338 | * Invoke this method to unregister a view hierarchy.
339 | *
340 | * @param view A view that belongs to the view hierarchy/window to unregister
341 | *
342 | * @see #addWindow(View, String)
343 | */
344 | public void removeWindow(View view) {
345 | View rootView;
346 | mWindowsLock.writeLock().lock();
347 | try {
348 | rootView = view.getRootView();
349 | mWindows.remove(rootView);
350 | } finally {
351 | mWindowsLock.writeLock().unlock();
352 | }
353 | mFocusLock.writeLock().lock();
354 | try {
355 | if (mFocusedWindow == rootView) {
356 | mFocusedWindow = null;
357 | }
358 | } finally {
359 | mFocusLock.writeLock().unlock();
360 | }
361 | fireWindowsChangedEvent();
362 | }
363 |
364 | /**
365 | * Invoke this method to change the currently focused window.
366 | *
367 | * @param activity The activity whose view hierarchy/window hasfocus,
368 | * or null to remove focus
369 | */
370 | public void setFocusedWindow(Activity activity) {
371 | setFocusedWindow(activity.getWindow().getDecorView());
372 | }
373 |
374 | /**
375 | * Invoke this method to change the currently focused window.
376 | *
377 | * @param view A view that belongs to the view hierarchy/window that has focus,
378 | * or null to remove focus
379 | */
380 | public void setFocusedWindow(View view) {
381 | mFocusLock.writeLock().lock();
382 | try {
383 | mFocusedWindow = view == null ? null : view.getRootView();
384 | } finally {
385 | mFocusLock.writeLock().unlock();
386 | }
387 | fireFocusChangedEvent();
388 | }
389 |
390 | /**
391 | * Main server loop.
392 | */
393 | public void run() {
394 | try {
395 | mServer = new ServerSocket(mPort, VIEW_SERVER_MAX_CONNECTIONS, InetAddress.getLocalHost());
396 | } catch (Exception e) {
397 | Log.w(LOG_TAG, "Starting ServerSocket error: ", e);
398 | }
399 |
400 | while (mServer != null && Thread.currentThread() == mThread) {
401 | // Any uncaught exception will crash the system process
402 | try {
403 | Socket client = mServer.accept();
404 | if (mThreadPool != null) {
405 | mThreadPool.submit(new ViewServerWorker(client));
406 | } else {
407 | try {
408 | client.close();
409 | } catch (IOException e) {
410 | e.printStackTrace();
411 | }
412 | }
413 | } catch (Exception e) {
414 | Log.w(LOG_TAG, "Connection error: ", e);
415 | }
416 | }
417 | }
418 |
419 | private static boolean writeValue(Socket client, String value) {
420 | boolean result;
421 | BufferedWriter out = null;
422 | try {
423 | OutputStream clientStream = client.getOutputStream();
424 | out = new BufferedWriter(new OutputStreamWriter(clientStream), 8 * 1024);
425 | out.write(value);
426 | out.write("\n");
427 | out.flush();
428 | result = true;
429 | } catch (Exception e) {
430 | result = false;
431 | } finally {
432 | if (out != null) {
433 | try {
434 | out.close();
435 | } catch (IOException e) {
436 | result = false;
437 | }
438 | }
439 | }
440 | return result;
441 | }
442 |
443 | private void fireWindowsChangedEvent() {
444 | for (WindowListener listener : mListeners) {
445 | listener.windowsChanged();
446 | }
447 | }
448 |
449 | private void fireFocusChangedEvent() {
450 | for (WindowListener listener : mListeners) {
451 | listener.focusChanged();
452 | }
453 | }
454 |
455 | private void addWindowListener(WindowListener listener) {
456 | if (!mListeners.contains(listener)) {
457 | mListeners.add(listener);
458 | }
459 | }
460 |
461 | private void removeWindowListener(WindowListener listener) {
462 | mListeners.remove(listener);
463 | }
464 |
465 | private interface WindowListener {
466 | void windowsChanged();
467 | void focusChanged();
468 | }
469 |
470 | private static class UncloseableOutputStream extends OutputStream {
471 | private final OutputStream mStream;
472 |
473 | UncloseableOutputStream(OutputStream stream) {
474 | mStream = stream;
475 | }
476 |
477 | public void close() throws IOException {
478 | // Don't close the stream
479 | }
480 |
481 | public boolean equals(Object o) {
482 | return mStream.equals(o);
483 | }
484 |
485 | public void flush() throws IOException {
486 | mStream.flush();
487 | }
488 |
489 | public int hashCode() {
490 | return mStream.hashCode();
491 | }
492 |
493 | public String toString() {
494 | return mStream.toString();
495 | }
496 |
497 | public void write(byte[] buffer, int offset, int count)
498 | throws IOException {
499 | mStream.write(buffer, offset, count);
500 | }
501 |
502 | public void write(byte[] buffer) throws IOException {
503 | mStream.write(buffer);
504 | }
505 |
506 | public void write(int oneByte) throws IOException {
507 | mStream.write(oneByte);
508 | }
509 | }
510 |
511 | private static class NoopViewServer extends ViewServer {
512 | private NoopViewServer() {
513 | }
514 |
515 | @Override
516 | public boolean start() throws IOException {
517 | return false;
518 | }
519 |
520 | @Override
521 | public boolean stop() {
522 | return false;
523 | }
524 |
525 | @Override
526 | public boolean isRunning() {
527 | return false;
528 | }
529 |
530 | @Override
531 | public void addWindow(Activity activity) {
532 | }
533 |
534 | @Override
535 | public void removeWindow(Activity activity) {
536 | }
537 |
538 | @Override
539 | public void addWindow(View view, String name) {
540 | }
541 |
542 | @Override
543 | public void removeWindow(View view) {
544 | }
545 |
546 | @Override
547 | public void setFocusedWindow(Activity activity) {
548 | }
549 |
550 | @Override
551 | public void setFocusedWindow(View view) {
552 | }
553 |
554 | @Override
555 | public void run() {
556 | }
557 | }
558 |
559 | private class ViewServerWorker implements Runnable, WindowListener {
560 | private Socket mClient;
561 | private boolean mNeedWindowListUpdate;
562 | private boolean mNeedFocusedWindowUpdate;
563 |
564 | private final Object[] mLock = new Object[0];
565 |
566 | public ViewServerWorker(Socket client) {
567 | mClient = client;
568 | mNeedWindowListUpdate = false;
569 | mNeedFocusedWindowUpdate = false;
570 | }
571 |
572 | public void run() {
573 | BufferedReader in = null;
574 | try {
575 | in = new BufferedReader(new InputStreamReader(mClient.getInputStream()), 1024);
576 |
577 | final String request = in.readLine();
578 |
579 | String command;
580 | String parameters;
581 |
582 | int index = request.indexOf(' ');
583 | if (index == -1) {
584 | command = request;
585 | parameters = "";
586 | } else {
587 | command = request.substring(0, index);
588 | parameters = request.substring(index + 1);
589 | }
590 |
591 | boolean result;
592 | if (COMMAND_PROTOCOL_VERSION.equalsIgnoreCase(command)) {
593 | result = writeValue(mClient, VALUE_PROTOCOL_VERSION);
594 | } else if (COMMAND_SERVER_VERSION.equalsIgnoreCase(command)) {
595 | result = writeValue(mClient, VALUE_SERVER_VERSION);
596 | } else if (COMMAND_WINDOW_MANAGER_LIST.equalsIgnoreCase(command)) {
597 | result = listWindows(mClient);
598 | } else if (COMMAND_WINDOW_MANAGER_GET_FOCUS.equalsIgnoreCase(command)) {
599 | result = getFocusedWindow(mClient);
600 | } else if (COMMAND_WINDOW_MANAGER_AUTOLIST.equalsIgnoreCase(command)) {
601 | result = windowManagerAutolistLoop();
602 | } else {
603 | result = windowCommand(mClient, command, parameters);
604 | }
605 |
606 | if (!result) {
607 | Log.w(LOG_TAG, "An error occurred with the command: " + command);
608 | }
609 | } catch(IOException e) {
610 | Log.w(LOG_TAG, "Connection error: ", e);
611 | } finally {
612 | if (in != null) {
613 | try {
614 | in.close();
615 |
616 | } catch (IOException e) {
617 | e.printStackTrace();
618 | }
619 | }
620 | if (mClient != null) {
621 | try {
622 | mClient.close();
623 | } catch (IOException e) {
624 | e.printStackTrace();
625 | }
626 | }
627 | }
628 | }
629 |
630 | private boolean windowCommand(Socket client, String command, String parameters) {
631 | boolean success = true;
632 | BufferedWriter out = null;
633 |
634 | try {
635 | // Find the hash code of the window
636 | int index = parameters.indexOf(' ');
637 | if (index == -1) {
638 | index = parameters.length();
639 | }
640 | final String code = parameters.substring(0, index);
641 | int hashCode = (int) Long.parseLong(code, 16);
642 |
643 | // Extract the command's parameter after the window description
644 | if (index < parameters.length()) {
645 | parameters = parameters.substring(index + 1);
646 | } else {
647 | parameters = "";
648 | }
649 |
650 | final View window = findWindow(hashCode);
651 | if (window == null) {
652 | return false;
653 | }
654 |
655 | // call stuff
656 | final Method dispatch = ViewDebug.class.getDeclaredMethod("dispatchCommand",
657 | View.class, String.class, String.class, OutputStream.class);
658 | dispatch.setAccessible(true);
659 | dispatch.invoke(null, window, command, parameters,
660 | new UncloseableOutputStream(client.getOutputStream()));
661 |
662 | if (!client.isOutputShutdown()) {
663 | out = new BufferedWriter(new OutputStreamWriter(client.getOutputStream()));
664 | out.write("DONE\n");
665 | out.flush();
666 | }
667 |
668 | } catch (Exception e) {
669 | Log.w(LOG_TAG, "Could not send command " + command +
670 | " with parameters " + parameters, e);
671 | success = false;
672 | } finally {
673 | if (out != null) {
674 | try {
675 | out.close();
676 | } catch (IOException e) {
677 | success = false;
678 | }
679 | }
680 | }
681 |
682 | return success;
683 | }
684 |
685 | private View findWindow(int hashCode) {
686 | if (hashCode == -1) {
687 | View window = null;
688 | mWindowsLock.readLock().lock();
689 | try {
690 | window = mFocusedWindow;
691 | } finally {
692 | mWindowsLock.readLock().unlock();
693 | }
694 | return window;
695 | }
696 |
697 |
698 | mWindowsLock.readLock().lock();
699 | try {
700 | for (Entry
17 | * Email: chjie.jaeger@gamil.com
18 | * GitHub: https://github.com/laobie
19 | */
20 | public class NineGridImageView