content: /**
   * Copyright (C) 2010-2016 eBusiness Information, Excilys Group
   * Copyright (C) 2016-2020 the AndroidAnnotations project
   *
   * Licensed under the Apache License, Version 2.0 (the "License"); you may not
   * use this file except in compliance with the License. You may obtain a copy of
   * the License at
   *
   * http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed To in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
   * License for the specific language governing permissions and limitations under
   * the License.
   */
  package org.androidannotations.api;
  
  import java.io.BufferedReader;
  import java.io.BufferedWriter;
  import java.io.IOException;
  import java.io.InputStreamReader;
  import java.io.OutputStream;
  import java.io.OutputStreamWriter;
  import java.lang.reflect.Method;
  import java.net.InetAddress;
  import java.net.ServerSocket;
  import java.net.Socket;
  import java.util.HashMap;
  import java.util.List;
  import java.util.Map;
  import java.util.Map.Entry;
  import java.util.concurrent.CopyOnWriteArrayList;
  import java.util.concurrent.ExecutorService;
  import java.util.concurrent.Executors;
  import java.util.concurrent.locks.ReentrantReadWriteLock;
  
  import android.app.Activity;
  import android.content.Context;
  import android.content.pm.ApplicationInfo;
  import android.os.Build;
  import android.text.TextUtils;
  import android.util.Log;
  import android.view.View;
  import android.view.ViewDebug;
  
  /**
   * <p>
   * This class can be used to enable the use of HierarchyViewer inside an
   * application. HierarchyViewer is an Android SDK tool that can be used to
   * inspect and debug the user interface of running applications. For security
   * reasons, HierarchyViewer does not work on production builds (for instance
   * phones bought in store.) By using this class, you can make HierarchyViewer
   * work on any device. You must be very careful however to only enable
   * HierarchyViewer when debugging your application.
   * </p>
   * 
   * <p>
   * To use this view server, your application must require the INTERNET
   * permission.
   * </p>
   * 
   * <p>
   * The recommended way to use this API is to register activities when they are
   * created, and to unregister them when they get destroyed:
   * </p>
   * 
   * <pre>
   * public class MyActivity extends Activity {
   * 	public void onCreate(Bundle savedInstanceState) {
   * 		super.onCreate(savedInstanceState);
   * 		// Set content view, etc.
   * 		ViewServer.get(this).addWindow(this);
   * 	}
   * 
   * 	public void onDestroy() {
   * 		super.onDestroy();
   * 		ViewServer.get(this).removeWindow(this);
   * 	}
   * 
   * 	public void onResume() {
   * 		super.onResume();
   * 		ViewServer.get(this).setFocusedWindow(this);
   * 	}
   * }
   * </pre>
   * 
   * <p>
   * In a similar fashion, you can use this API with an InputMethodService:
   * </p>
   * 
   * <pre>
   * public class MyInputMethodService extends InputMethodService {
   * 	public void onCreate() {
   * 		super.onCreate();
   * 		View decorView = getWindow().getWindow().getDecorView();
   * 		String name = &quot;MyInputMethodService&quot;;
   * 		ViewServer.get(this).addWindow(decorView, name);
   * 	}
   * 
   * 	public void onDestroy() {
   * 		super.onDestroy();
   * 		View decorView = getWindow().getWindow().getDecorView();
   * 		ViewServer.get(this).removeWindow(decorView);
   * 	}
   * 
   * 	public void onStartInput(EditorInfo attribute, boolean restarting) {
   * 		super.onStartInput(attribute, restarting);
   * 		View decorView = getWindow().getWindow().getDecorView();
   * 		ViewServer.get(this).setFocusedWindow(decorView);
   * 	}
   * }
   * </pre>
   */
  @SuppressWarnings("checkstyle:finalclass")
  public class ViewServer implements Runnable {
  	/**
  	 * The default port used to start view servers.
  	 */
  	private static final int VIEW_SERVER_DEFAULT_PORT = 4939;
  	private static final int VIEW_SERVER_MAX_CONNECTIONS = 10;
  	private static final String BUILD_TYPE_USER = "user";
  
  	// Debug facility
  	private static final String LOG_TAG = "ViewServer";
  
  	private static final String VALUE_PROTOCOL_VERSION = "4";
  	private static final String VALUE_SERVER_VERSION = "4";
  
  	// Protocol commands
  	// Returns the protocol version
  	private static final String COMMAND_PROTOCOL_VERSION = "PROTOCOL";
  	// Returns the server version
  	private static final String COMMAND_SERVER_VERSION = "SERVER";
  	// Lists all of the available windows in the system
  	private static final String COMMAND_WINDOW_MANAGER_LIST = "LIST";
  	// Keeps a connection open and notifies when the list of windows changes
  	private static final String COMMAND_WINDOW_MANAGER_AUTOLIST = "AUTOLIST";
  	// Returns the focused window
  	private static final String COMMAND_WINDOW_MANAGER_GET_FOCUS = "GET_FOCUS";
  
  	private ServerSocket mServer;
  	private final int mPort;
  
  	private Thread mThread;
  	private ExecutorService mThreadPool;
  
  	private final List<WindowListener> mListeners = new CopyOnWriteArrayList<>();
  
  	private final Map<View, String> mWindows = new HashMap<>();
  	private final ReentrantReadWriteLock mWindowsLock = new ReentrantReadWriteLock();
  
  	private View mFocusedWindow;
  	private final ReentrantReadWriteLock mFocusLock = new ReentrantReadWriteLock();
  
  	private static ViewServer sServer;
  
  	/**
  	 * Returns a unique instance of the ViewServer. This method should only be
  	 * called from the main thread of your application. The server will have the
  	 * same lifetime as your process.
  	 * 
  	 * If your application does not have the <code>android:debuggable</code> flag
  	 * set in its manifest, the server returned by this method will be a dummy
  	 * object that does not do anything. This allows you to use the same code in
  	 * debug and release versions of your application.
  	 * 
  	 * @param context
  	 *            A Context used to check whether the application is debuggable,
  	 *            this can be the application context
  	 * @return the instance of the view server.
  	 */
  	public static ViewServer get(Context context) {
  		ApplicationInfo info = context.getApplicationInfo();
  		if (BUILD_TYPE_USER.equals(Build.TYPE) && (info.flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0) {
  			if (sServer == null) {
  				sServer = new ViewServer(ViewServer.VIEW_SERVER_DEFAULT_PORT);
  			}
  
  			if (!sServer.isRunning()) {
  				try {
  					sServer.start();
  				} catch (IOException e) {
  					Log.d(LOG_TAG, "Error:", e);
  				}
  			}
  		} else {
  			sServer = new NoopViewServer();
  		}
  
  		return sServer;
  	}
  
  	private ViewServer() {
  		mPort = -1;
  	}
  
  	/**
  	 * Creates a new ViewServer associated with the specified window manager on the
  	 * specified local port. The server is not started by default.
  	 * 
  	 * @param port
  	 *            The port for the server to listen to.
  	 * 
  	 * @see #start()
  	 */
  	private ViewServer(int port) {
  		mPort = port;
  	}
  
  	/**
  	 * Starts the server.
  	 * 
  	 * @return True if the server was successfully created, or false if it already
  	 *         exists.
  	 * @throws IOException
  	 *             If the server cannot be created.
  	 * 
  	 * @see #stop()
  	 * @see #isRunning() see WindowManagerService#startViewServer(int)
  	 */
  	public boolean start() throws IOException {
  		if (mThread != null) {
  			return false;
  		}
  
  		mThread = new Thread(this, "Local View Server [port=" + mPort + "]");
  		mThreadPool = Executors.newFixedThreadPool(VIEW_SERVER_MAX_CONNECTIONS);
  		mThread.start();
  
  		return true;
  	}
  
  	/**
  	 * Stops the server.
  	 * 
  	 * @return True if the server was stopped, false if an error occurred or if the
  	 *         server wasn't started.
  	 * 
  	 * @see #start()
  	 * @see #isRunning() see WindowManagerService#stopViewServer()
  	 */
  	public boolean stop() {
  		if (mThread != null) {
  			mThread.interrupt();
  			if (mThreadPool != null) {
  				try {
  					mThreadPool.shutdownNow();
  				} catch (SecurityException e) {
  					Log.w(LOG_TAG, "Could not stop all view server threads");
  				}
  			}
  
  			mThreadPool = null;
  			mThread = null;
  
  			try {
  				mServer.close();
  				mServer = null;
  				return true;
  			} catch (IOException e) {
  				Log.w(LOG_TAG, "Could not close the view server");
  			}
  		}
  
  		mWindowsLock.writeLock().lock();
  		try {
  			mWindows.clear();
  		} finally {
  			mWindowsLock.writeLock().unlock();
  		}
  
  		mFocusLock.writeLock().lock();
  		try {
  			mFocusedWindow = null;
  		} finally {
  			mFocusLock.writeLock().unlock();
  		}
  
  		return false;
  	}
  
  	/**
  	 * Indicates whether the server is currently running.
  	 * 
  	 * @return True if the server is running, false otherwise.
  	 * 
  	 * @see #start()
  	 * @see #stop() see WindowManagerService#isViewServerRunning()
  	 */
  	public boolean isRunning() {
  		return mThread != null && mThread.isAlive();
  	}
  
  	/**
  	 * Invoke this method to register a new view hierarchy.
  	 * 
  	 * @param activity
  	 *            The activity whose view hierarchy/window to register
  	 * 
  	 * @see #addWindow(View, String)
  	 * @see #removeWindow(Activity)
  	 */
  	public void addWindow(Activity activity) {
  		String name = activity.getTitle().toString();
  		if (TextUtils.isEmpty(name)) {
  			name = activity.getClass().getCanonicalName() + "/0x" + System.identityHashCode(activity);
  		} else {
  			name += "(" + activity.getClass().getCanonicalName() + ")";
  		}
  		addWindow(activity.getWindow().getDecorView(), name);
  	}
  
  	/**
  	 * Invoke this method to unregister a view hierarchy.
  	 * 
  	 * @param activity
  	 *            The activity whose view hierarchy/window to unregister
  	 * 
  	 * @see #addWindow(Activity)
  	 * @see #removeWindow(View)
  	 */
  	public void removeWindow(Activity activity) {
  		removeWindow(activity.getWindow().getDecorView());
  	}
  
  	/**
  	 * Invoke this method to register a new view hierarchy.
  	 * 
  	 * @param view
  	 *            A view that belongs to the view hierarchy/window to register
  	 * @param name
  	 *            The name of the view hierarchy/window to register
  	 * 
  	 * @see #removeWindow(View)
  	 */
  	public void addWindow(View view, String name) {
  		mWindowsLock.writeLock().lock();
  		try {
  			mWindows.put(view.getRootView(), name);
  		} finally {
  			mWindowsLock.writeLock().unlock();
  		}
  		fireWindowsChangedEvent();
  	}
  
  	/**
  	 * Invoke this method to unregister a view hierarchy.
  	 * 
  	 * @param view
  	 *            A view that belongs to the view hierarchy/window to unregister
  	 * 
  	 * @see #addWindow(View, String)
  	 */
  	public void removeWindow(View view) {
  		View rootView;
  		mWindowsLock.writeLock().lock();
  		try {
  			rootView = view.getRootView();
  			mWindows.remove(rootView);
  		} finally {
  			mWindowsLock.writeLock().unlock();
  		}
  		mFocusLock.writeLock().lock();
  		try {
  			if (mFocusedWindow == rootView) {
  				mFocusedWindow = null;
  			}
  		} finally {
  			mFocusLock.writeLock().unlock();
  		}
  		fireWindowsChangedEvent();
  	}
  
  	/**
  	 * Invoke this method to change the currently focused window.
  	 * 
  	 * @param activity
  	 *            The activity whose view hierarchy/window hasfocus, or null to
  	 *            remove focus
  	 */
  	public void setFocusedWindow(Activity activity) {
  		setFocusedWindow(activity.getWindow().getDecorView());
  	}
  
  	/**
  	 * Invoke this method to change the currently focused window.
  	 * 
  	 * @param view
  	 *            A view that belongs to the view hierarchy/window that has focus,
  	 *            or null to remove focus
  	 */
  	public void setFocusedWindow(View view) {
  		mFocusLock.writeLock().lock();
  		try {
  			mFocusedWindow = view == null ? null : view.getRootView();
  		} finally {
  			mFocusLock.writeLock().unlock();
  		}
  		fireFocusChangedEvent();
  	}
  
  	/**
  	 * Main server loop.
  	 */
  	@Override
  	public void run() {
  		try {
  			mServer = new ServerSocket(mPort, VIEW_SERVER_MAX_CONNECTIONS, InetAddress.getLocalHost());
  		} catch (Exception e) {
  			Log.w(LOG_TAG, "Starting ServerSocket error: ", e);
  		}
  
  		while (mServer != null && Thread.currentThread() == mThread) {
  			// Any uncaught exception will crash the system process
  			try {
  				Socket client = mServer.accept();
  				if (mThreadPool != null) {
  					mThreadPool.submit(new ViewServerWorker(client));
  				} else {
  					try {
  						client.close();
  					} catch (IOException e) {
  						e.printStackTrace();
  					}
  				}
  			} catch (Exception e) {
  				Log.w(LOG_TAG, "Connection error: ", e);
  			}
  		}
  	}
  
  	private static boolean writeValue(Socket client, String value) {
  		boolean result;
  		BufferedWriter out = null;
  		try {
  			OutputStream clientStream = client.getOutputStream();
  			out = new BufferedWriter(new OutputStreamWriter(clientStream), 8 * 1024);
  			out.write(value);
  			out.write("\n");
  			out.flush();
  			result = true;
  		} catch (Exception e) {
  			result = false;
  		} finally {
  			if (out != null) {
  				try {
  					out.close();
  				} catch (IOException e) {
  					result = false;
  				}
  			}
  		}
  		return result;
  	}
  
  	private void fireWindowsChangedEvent() {
  		for (WindowListener listener : mListeners) {
  			listener.windowsChanged();
  		}
  	}
  
  	private void fireFocusChangedEvent() {
  		for (WindowListener listener : mListeners) {
  			listener.focusChanged();
  		}
  	}
  
  	private void addWindowListener(WindowListener listener) {
  		if (!mListeners.contains(listener)) {
  			mListeners.add(listener);
  		}
  	}
  
  	private void removeWindowListener(WindowListener listener) {
  		mListeners.remove(listener);
  	}
  
  	private interface WindowListener {
  		void windowsChanged();
  
  		void focusChanged();
  	}
  
  	private static class UncloseableOutputStream extends OutputStream {
  		private final OutputStream mStream;
  
  		UncloseableOutputStream(OutputStream stream) {
  			mStream = stream;
  		}
  
  		@Override
  		public void close() throws IOException {
  			// Don't close the stream
  		}
  
  		@Override
  		public boolean equals(Object o) {
  			return mStream.equals(o);
  		}
  
  		@Override
  		public void flush() throws IOException {
  			mStream.flush();
  		}
  
  		@Override
  		public int hashCode() {
  			return mStream.hashCode();
  		}
  
  		@Override
  		public String toString() {
  			return mStream.toString();
  		}
  
  		@Override
  		public void write(byte[] buffer, int offset, int count) throws IOException {
  			mStream.write(buffer, offset, count);
  		}
  
  		@Override
  		public void write(byte[] buffer) throws IOException {
  			mStream.write(buffer);
  		}
  
  		@Override
  		public void write(int oneByte) throws IOException {
  			mStream.write(oneByte);
  		}
  	}
  
  	private static final class NoopViewServer extends ViewServer {
  		private NoopViewServer() {
  		}
  
  		@Override
  		public boolean start() throws IOException {
  			return false;
  		}
  
  		@Override
  		public boolean stop() {
  			return false;
  		}
  
  		@Override
  		public boolean isRunning() {
  			return false;
  		}
  
  		@Override
  		public void addWindow(Activity activity) {
  		}
  
  		@Override
  		public void removeWindow(Activity activity) {
  		}
  
  		@Override
  		public void addWindow(View view, String name) {
  		}
  
  		@Override
  		public void removeWindow(View view) {
  		}
  
  		@Override
  		public void setFocusedWindow(Activity activity) {
  		}
  
  		@Override
  		public void setFocusedWindow(View view) {
  		}
  
  		@Override
  		public void run() {
  		}
  	}
  
  	private class ViewServerWorker implements Runnable, WindowListener {
  		private Socket mClient;
  		private boolean mNeedWindowListUpdate;
  		private boolean mNeedFocusedWindowUpdate;
  
  		private final Object[] mLock = new Object[0];
  
  		private ViewServerWorker(Socket client) {
  			mClient = client;
  			mNeedWindowListUpdate = false;
  			mNeedFocusedWindowUpdate = false;
  		}
  
  		@Override
  		public void run() {
  			BufferedReader in = null;
  			try {
  				in = new BufferedReader(new InputStreamReader(mClient.getInputStream()), 1024);
  
  				final String request = in.readLine();
  
  				String command;
  				String parameters;
  
  				int index = request.indexOf(' ');
  				if (index == -1) {
  					command = request;
  					parameters = "";
  				} else {
  					command = request.substring(0, index);
  					parameters = request.substring(index + 1);
  				}
  
  				boolean result;
  				if (COMMAND_PROTOCOL_VERSION.equalsIgnoreCase(command)) {
  					result = writeValue(mClient, VALUE_PROTOCOL_VERSION);
  				} else if (COMMAND_SERVER_VERSION.equalsIgnoreCase(command)) {
  					result = writeValue(mClient, VALUE_SERVER_VERSION);
  				} else if (COMMAND_WINDOW_MANAGER_LIST.equalsIgnoreCase(command)) {
  					result = listWindows(mClient);
  				} else if (COMMAND_WINDOW_MANAGER_GET_FOCUS.equalsIgnoreCase(command)) {
  					result = getFocusedWindow(mClient);
  				} else if (COMMAND_WINDOW_MANAGER_AUTOLIST.equalsIgnoreCase(command)) {
  					result = windowManagerAutolistLoop();
  				} else {
  					result = windowCommand(mClient, command, parameters);
  				}
  
  				if (!result) {
  					Log.w(LOG_TAG, "An error occurred with the command: " + command);
  				}
  			} catch (IOException e) {
  				Log.w(LOG_TAG, "Connection error: ", e);
  			} finally {
  				if (in != null) {
  					try {
  						in.close();
  
  					} catch (IOException e) {
  						e.printStackTrace();
  					}
  				}
  				if (mClient != null) {
  					try {
  						mClient.close();
  					} catch (IOException e) {
  						e.printStackTrace();
  					}
  				}
  			}
  		}
  
  		private boolean windowCommand(Socket client, String command, String parameters) {
  			boolean success = true;
  			BufferedWriter out = null;
  
  			try {
  				// Find the hash code of the window
  				int index = parameters.indexOf(' ');
  				if (index == -1) {
  					index = parameters.length();
  				}
  				final String code = parameters.substring(0, index);
  				int hashCode = (int) Long.parseLong(code, 16);
  
  				// Extract the command's parameter after the window description
  				if (index < parameters.length()) {
  					parameters = parameters.substring(index + 1);
  				} else {
  					parameters = "";
  				}
  
  				final View window = findWindow(hashCode);
  				if (window == null) {
  					return false;
  				}
  
  				// call stuff
  				final Method dispatch = ViewDebug.class.getDeclaredMethod("dispatchCommand", View.class, String.class, String.class, OutputStream.class);
  				dispatch.setAccessible(true);
  				dispatch.invoke(null, window, command, parameters, new UncloseableOutputStream(client.getOutputStream()));
  
  				if (!client.isOutputShutdown()) {
  					out = new BufferedWriter(new OutputStreamWriter(client.getOutputStream()));
  					out.write("DONE\n");
  					out.flush();
  				}
  
  			} catch (Exception e) {
  				Log.w(LOG_TAG, "Could not send command " + command + " with parameters " + parameters, e);
  				success = false;
  			} finally {
  				if (out != null) {
  					try {
  						out.close();
  					} catch (IOException e) {
  						success = false;
  					}
  				}
  			}
  
  			return success;
  		}
  
  		private View findWindow(int hashCode) {
  			if (hashCode == -1) {
  				View window = null;
  				mWindowsLock.readLock().lock();
  				try {
  					window = mFocusedWindow;
  				} finally {
  					mWindowsLock.readLock().unlock();
  				}
  				return window;
  			}
  
  			mWindowsLock.readLock().lock();
  			try {
  				for (Entry<View, String> entry : mWindows.entrySet()) {
  					if (System.identityHashCode(entry.getKey()) == hashCode) {
  						return entry.getKey();
  					}
  				}
  			} finally {
  				mWindowsLock.readLock().unlock();
  			}
  
  			return null;
  		}
  
  		private boolean listWindows(Socket client) {
  			boolean result = true;
  			BufferedWriter out = null;
  
  			try {
  				mWindowsLock.readLock().lock();
  
  				OutputStream clientStream = client.getOutputStream();
  				out = new BufferedWriter(new OutputStreamWriter(clientStream), 8 * 1024);
  
  				for (Entry<View, String> entry : mWindows.entrySet()) {
  					out.write(Integer.toHexString(System.identityHashCode(entry.getKey())));
  					out.write(' ');
  					out.append(entry.getValue());
  					out.write('\n');
  				}
  
  				out.write("DONE.\n");
  				out.flush();
  			} catch (Exception e) {
  				result = false;
  			} finally {
  				mWindowsLock.readLock().unlock();
  
  				if (out != null) {
  					try {
  						out.close();
  					} catch (IOException e) {
  						result = false;
  					}
  				}
  			}
  
  			return result;
  		}
  
  		private boolean getFocusedWindow(Socket client) {
  			boolean result = true;
  			String focusName = null;
  
  			BufferedWriter out = null;
  			try {
  				OutputStream clientStream = client.getOutputStream();
  				out = new BufferedWriter(new OutputStreamWriter(clientStream), 8 * 1024);
  
  				View focusedWindow = null;
  
  				mFocusLock.readLock().lock();
  				try {
  					focusedWindow = mFocusedWindow;
  				} finally {
  					mFocusLock.readLock().unlock();
  				}
  
  				if (focusedWindow != null) {
  					mWindowsLock.readLock().lock();
  					try {
  						focusName = mWindows.get(mFocusedWindow);
  					} finally {
  						mWindowsLock.readLock().unlock();
  					}
  
  					out.write(Integer.toHexString(System.identityHashCode(focusedWindow)));
  					out.write(' ');
  					out.append(focusName);
  				}
  				out.write('\n');
  				out.flush();
  			} catch (Exception e) {
  				result = false;
  			} finally {
  				if (out != null) {
  					try {
  						out.close();
  					} catch (IOException e) {
  						result = false;
  					}
  				}
  			}
  
  			return result;
  		}
  
  		@Override
  		public void windowsChanged() {
  			synchronized (mLock) {
  				mNeedWindowListUpdate = true;
  				mLock.notifyAll();
  			}
  		}
  
  		@Override
  		public void focusChanged() {
  			synchronized (mLock) {
  				mNeedFocusedWindowUpdate = true;
  				mLock.notifyAll();
  			}
  		}
  
  		private boolean windowManagerAutolistLoop() {
  			addWindowListener(this);
  			BufferedWriter out = null;
  			try {
  				out = new BufferedWriter(new OutputStreamWriter(mClient.getOutputStream()));
  				while (!Thread.interrupted()) {
  					boolean needWindowListUpdate = false;
  					boolean needFocusedWindowUpdate = false;
  					synchronized (mLock) {
  						while (!mNeedWindowListUpdate && !mNeedFocusedWindowUpdate) {
  							mLock.wait();
  						}
  						if (mNeedWindowListUpdate) {
  							mNeedWindowListUpdate = false;
  							needWindowListUpdate = true;
  						}
  						if (mNeedFocusedWindowUpdate) {
  							mNeedFocusedWindowUpdate = false;
  							needFocusedWindowUpdate = true;
  						}
  					}
  					if (needWindowListUpdate) {
  						out.write("LIST UPDATE\n");
  						out.flush();
  					}
  					if (needFocusedWindowUpdate) {
  						out.write("FOCUS UPDATE\n");
  						out.flush();
  					}
  				}
  			} catch (Exception e) {
  				Log.w(LOG_TAG, "Connection error: ", e);
  			} finally {
  				if (out != null) {
  					try {
  						out.close();
  					} catch (IOException e) {
  						// Ignore
  					}
  				}
  				removeWindowListener(this);
  			}
  			return true;
  		}
  	}
  }
  