zoukankan      html  css  js  c++  java
  • [原创]HierarchyView的实现原理和Android设备无法使用HierarchyView的解决方法

    最近在看一个老外写的东西,发现里面有个类,使用这个类可以让任何设备使用HierarchyView。

    众所周知,市面上卖的Android设备,一般都不能使用HierarchyView,所以借此机会,了解一下HierarchyView的实现原理,并学习一下老外的解决方法。

    HierarchyView的源码在/sdk/eclipse/plugins/com.android.ide.eclipse.hierarchyviewer中,但貌似不全,

    所以直接反编译/prebuilts/devtools/tools/lib/hierarchyviewer2lib.jar和/prebuilts/devtools/tools/lib/hierarchyviewer2.jar。

    当对设备使用HierarchyView时,HierarchyView会给设备发送一个startViewServer的命令,下面源码时其调用顺序:

    HierarchyViewerDirector.class

      public void populateDeviceSelectionModel() {
        IDevice[] devices = DeviceBridge.getDevices();
        for (IDevice device : devices)
          deviceConnected(device);
      }
    
      public void deviceConnected(final IDevice device)
      {
        executeInBackground("Connecting device", new Object()
        {
          public void run() {
            if (!device.isOnline())
              return;
            IHvDevice hvDevice;
            synchronized (HierarchyViewerDirector.mDevicesLock) {
              hvDevice = (IHvDevice)HierarchyViewerDirector.this.mDevices.get(device);
              if (hvDevice == null) {
                hvDevice = HvDeviceFactory.create(device);
                hvDevice.initializeViewDebug();
                hvDevice.addWindowChangeListener(HierarchyViewerDirector.getDirector());
                HierarchyViewerDirector.this.mDevices.put(device, hvDevice);
              }
              else {
                hvDevice.initializeViewDebug();
              }
            }
    
            DeviceSelectionModel.getModel().addDevice(hvDevice);
            HierarchyViewerDirector.this.focusChanged(device);
          }
        });
      }

    ViewServerDevice.class

      public boolean initializeViewDebug()
      {
        if (!this.mDevice.isOnline()) {
          return false;
        }
    
        DeviceBridge.setupDeviceForward(this.mDevice);
    
        return reloadWindows();
      }
    
      public boolean reloadWindows()
      {
        if ((!DeviceBridge.isViewServerRunning(this.mDevice)) && 
          (!DeviceBridge.startViewServer(this.mDevice))) {
          Log.e("ViewServerDevice", "Unable to debug device: " + this.mDevice.getName());
          DeviceBridge.removeDeviceForward(this.mDevice);
          return false;
        }
    
        this.mViewServerInfo = DeviceBridge.loadViewServerInfo(this.mDevice);
        if (this.mViewServerInfo == null) {
          return false;
        }
    
        this.mWindows = DeviceBridge.loadWindows(this, this.mDevice);
        return true;
      }

    DeviceBridge.class

      public static boolean startViewServer(IDevice device) {
        return startViewServer(device, 4939);
      }
    
      public static boolean startViewServer(IDevice device, int port) {
        boolean[] result = new boolean[1];
        try {
          if (device.isOnline())
            device.executeShellCommand(buildStartServerShellCommand(port), new BooleanResultReader(result));
        }
        catch (TimeoutException e)
        {
          Log.e("hierarchyviewer", "Timeout starting view server on device " + device);
        } catch (IOException e) {
          Log.e("hierarchyviewer", "Unable to start view server on device " + device);
        } catch (AdbCommandRejectedException e) {
          Log.e("hierarchyviewer", "Adb rejected command to start view server on device " + device);
        } catch (ShellCommandUnresponsiveException e) {
          Log.e("hierarchyviewer", "Unable to execute command to start view server on device " + device);
        }
        return result[0];
      }
    
      private static String buildStartServerShellCommand(int port) {
        return String.format("service call window %d i32 %d", new Object[] { Integer.valueOf(1), Integer.valueOf(port) });
      }

    从代码中可以看到,最终HierarchyView会让设备执行service命令,最终执行的命令是这样:

    shell@device:/ $ service call window 1 i32 4939

    这行命令其实是向android.view.IWindowManager发送一个CODE为1,值为4939的parcel。

    其实就是调用WindowManagerService中的startViewServer方法,并把4939作为参数传入,接下来看看WindowManagerService.startViewServer的源码:

        public boolean startViewServer(int port) {
            if (isSystemSecure()) {
                return false;
            }
    
            if (!checkCallingPermission(Manifest.permission.DUMP, "startViewServer")) {
                return false;
            }
    
            if (port < 1024) {
                return false;
            }
    
            if (mViewServer != null) {
                if (!mViewServer.isRunning()) {
                    try {
                        return mViewServer.start();
                    } catch (IOException e) {
                        Slog.w(TAG, "View server did not start");
                    }
                }
                return false;
            }
    
            try {
                mViewServer = new ViewServer(this, port);
                return mViewServer.start();
            } catch (IOException e) {
                Slog.w(TAG, "View server did not start");
            }
            return false;
        }
    
        private boolean isSystemSecure() {
            return "1".equals(SystemProperties.get(SYSTEM_SECURE, "1")) &&
                    "0".equals(SystemProperties.get(SYSTEM_DEBUGGABLE, "0"));
        }

    里面会做一些权限检查,然后会调用ViewServer.start(),关键就在ViewServer里,先吧ViewServer完整的代码贴上:

      1 /*
      2  * Copyright (C) 2007 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.server.wm;
     18 
     19 
     20 import android.util.Slog;
     21 
     22 import java.net.ServerSocket;
     23 import java.net.Socket;
     24 import java.net.InetAddress;
     25 import java.util.concurrent.ExecutorService;
     26 import java.util.concurrent.Executors;
     27 import java.io.IOException;
     28 import java.io.BufferedReader;
     29 import java.io.InputStreamReader;
     30 import java.io.OutputStream;
     31 import java.io.BufferedWriter;
     32 import java.io.OutputStreamWriter;
     33 
     34 /**
     35  * The ViewServer is local socket server that can be used to communicate with the
     36  * views of the opened windows. Communication with the views is ensured by the
     37  * {@link com.android.server.wm.WindowManagerService} and is a cross-process operation.
     38  *
     39  * {@hide}
     40  */
     41 class ViewServer implements Runnable {
     42     /**
     43      * The default port used to start view servers.
     44      */
     45     public static final int VIEW_SERVER_DEFAULT_PORT = 4939;
     46 
     47     private static final int VIEW_SERVER_MAX_CONNECTIONS = 10;
     48 
     49     // Debug facility
     50     private static final String LOG_TAG = "ViewServer";
     51 
     52     private static final String VALUE_PROTOCOL_VERSION = "4";
     53     private static final String VALUE_SERVER_VERSION = "4";
     54 
     55     // Protocol commands
     56     // Returns the protocol version
     57     private static final String COMMAND_PROTOCOL_VERSION = "PROTOCOL";
     58     // Returns the server version
     59     private static final String COMMAND_SERVER_VERSION = "SERVER";
     60     // Lists all of the available windows in the system
     61     private static final String COMMAND_WINDOW_MANAGER_LIST = "LIST";
     62     // Keeps a connection open and notifies when the list of windows changes
     63     private static final String COMMAND_WINDOW_MANAGER_AUTOLIST = "AUTOLIST";
     64     // Returns the focused window
     65     private static final String COMMAND_WINDOW_MANAGER_GET_FOCUS = "GET_FOCUS";
     66 
     67     private ServerSocket mServer;
     68     private Thread mThread;
     69 
     70     private final WindowManagerService mWindowManager;
     71     private final int mPort;
     72 
     73     private ExecutorService mThreadPool;
     74 
     75     /**
     76      * Creates a new ViewServer associated with the specified window manager on the
     77      * specified local port. The server is not started by default.
     78      *
     79      * @param windowManager The window manager used to communicate with the views.
     80      * @param port The port for the server to listen to.
     81      *
     82      * @see #start()
     83      */
     84     ViewServer(WindowManagerService windowManager, int port) {
     85         mWindowManager = windowManager;
     86         mPort = port;
     87     }
     88 
     89     /**
     90      * Starts the server.
     91      *
     92      * @return True if the server was successfully created, or false if it already exists.
     93      * @throws IOException If the server cannot be created.
     94      *
     95      * @see #stop()
     96      * @see #isRunning()
     97      * @see WindowManagerService#startViewServer(int)
     98      */
     99     boolean start() throws IOException {
    100         if (mThread != null) {
    101             return false;
    102         }
    103 
    104         mServer = new ServerSocket(mPort, VIEW_SERVER_MAX_CONNECTIONS, InetAddress.getLocalHost());
    105         mThread = new Thread(this, "Remote View Server [port=" + mPort + "]");
    106         mThreadPool = Executors.newFixedThreadPool(VIEW_SERVER_MAX_CONNECTIONS);
    107         mThread.start();
    108 
    109         return true;
    110     }
    111 
    112     /**
    113      * Stops the server.
    114      *
    115      * @return True if the server was stopped, false if an error occured or if the
    116      *         server wasn't started.
    117      *
    118      * @see #start()
    119      * @see #isRunning()
    120      * @see WindowManagerService#stopViewServer()
    121      */
    122     boolean stop() {
    123         if (mThread != null) {
    124 
    125             mThread.interrupt();
    126             if (mThreadPool != null) {
    127                 try {
    128                     mThreadPool.shutdownNow();
    129                 } catch (SecurityException e) {
    130                     Slog.w(LOG_TAG, "Could not stop all view server threads");
    131                 }
    132             }
    133             mThreadPool = null;
    134             mThread = null;
    135             try {
    136                 mServer.close();
    137                 mServer = null;
    138                 return true;
    139             } catch (IOException e) {
    140                 Slog.w(LOG_TAG, "Could not close the view server");
    141             }
    142         }
    143         return false;
    144     }
    145 
    146     /**
    147      * Indicates whether the server is currently running.
    148      *
    149      * @return True if the server is running, false otherwise.
    150      *
    151      * @see #start()
    152      * @see #stop()
    153      * @see WindowManagerService#isViewServerRunning()  
    154      */
    155     boolean isRunning() {
    156         return mThread != null && mThread.isAlive();
    157     }
    158 
    159     /**
    160      * Main server loop.
    161      */
    162     public void run() {
    163         while (Thread.currentThread() == mThread) {
    164             // Any uncaught exception will crash the system process
    165             try {
    166                 Socket client = mServer.accept();
    167                 if (mThreadPool != null) {
    168                     mThreadPool.submit(new ViewServerWorker(client));
    169                 } else {
    170                     try {
    171                         client.close();
    172                     } catch (IOException e) {
    173                         e.printStackTrace();
    174                     }
    175                 }
    176             } catch (Exception e) {
    177                 Slog.w(LOG_TAG, "Connection error: ", e);
    178             }
    179         }
    180     }
    181 
    182     private static boolean writeValue(Socket client, String value) {
    183         boolean result;
    184         BufferedWriter out = null;
    185         try {
    186             OutputStream clientStream = client.getOutputStream();
    187             out = new BufferedWriter(new OutputStreamWriter(clientStream), 8 * 1024);
    188             out.write(value);
    189             out.write("
    ");
    190             out.flush();
    191             result = true;
    192         } catch (Exception e) {
    193             result = false;
    194         } finally {
    195             if (out != null) {
    196                 try {
    197                     out.close();
    198                 } catch (IOException e) {
    199                     result = false;
    200                 }
    201             }
    202         }
    203         return result;
    204     }
    205 
    206     class ViewServerWorker implements Runnable, WindowManagerService.WindowChangeListener {
    207         private Socket mClient;
    208         private boolean mNeedWindowListUpdate;
    209         private boolean mNeedFocusedWindowUpdate;
    210 
    211         public ViewServerWorker(Socket client) {
    212             mClient = client;
    213             mNeedWindowListUpdate = false;
    214             mNeedFocusedWindowUpdate = false;
    215         }
    216 
    217         public void run() {
    218 
    219             BufferedReader in = null;
    220             try {
    221                 in = new BufferedReader(new InputStreamReader(mClient.getInputStream()), 1024);
    222 
    223                 final String request = in.readLine();
    224 
    225                 String command;
    226                 String parameters;
    227 
    228                 int index = request.indexOf(' ');
    229                 if (index == -1) {
    230                     command = request;
    231                     parameters = "";
    232                 } else {
    233                     command = request.substring(0, index);
    234                     parameters = request.substring(index + 1);
    235                 }
    236 
    237                 boolean result;
    238                 if (COMMAND_PROTOCOL_VERSION.equalsIgnoreCase(command)) {
    239                     result = writeValue(mClient, VALUE_PROTOCOL_VERSION);
    240                 } else if (COMMAND_SERVER_VERSION.equalsIgnoreCase(command)) {
    241                     result = writeValue(mClient, VALUE_SERVER_VERSION);
    242                 } else if (COMMAND_WINDOW_MANAGER_LIST.equalsIgnoreCase(command)) {
    243                     result = mWindowManager.viewServerListWindows(mClient);
    244                 } else if (COMMAND_WINDOW_MANAGER_GET_FOCUS.equalsIgnoreCase(command)) {
    245                     result = mWindowManager.viewServerGetFocusedWindow(mClient);
    246                 } else if (COMMAND_WINDOW_MANAGER_AUTOLIST.equalsIgnoreCase(command)) {
    247                     result = windowManagerAutolistLoop();
    248                 } else {
    249                     result = mWindowManager.viewServerWindowCommand(mClient,
    250                             command, parameters);
    251                 }
    252 
    253                 if (!result) {
    254                     Slog.w(LOG_TAG, "An error occurred with the command: " + command);
    255                 }
    256             } catch(IOException e) {
    257                 Slog.w(LOG_TAG, "Connection error: ", e);
    258             } finally {
    259                 if (in != null) {
    260                     try {
    261                         in.close();
    262 
    263                     } catch (IOException e) {
    264                         e.printStackTrace();
    265                     }
    266                 }
    267                 if (mClient != null) {
    268                     try {
    269                         mClient.close();
    270                     } catch (IOException e) {
    271                         e.printStackTrace();
    272                     }
    273                 }
    274             }
    275         }
    276 
    277         public void windowsChanged() {
    278             synchronized(this) {
    279                 mNeedWindowListUpdate = true;
    280                 notifyAll();
    281             }
    282         }
    283 
    284         public void focusChanged() {
    285             synchronized(this) {
    286                 mNeedFocusedWindowUpdate = true;
    287                 notifyAll();
    288             }
    289         }
    290 
    291         private boolean windowManagerAutolistLoop() {
    292             mWindowManager.addWindowChangeListener(this);
    293             BufferedWriter out = null;
    294             try {
    295                 out = new BufferedWriter(new OutputStreamWriter(mClient.getOutputStream()));
    296                 while (!Thread.interrupted()) {
    297                     boolean needWindowListUpdate = false;
    298                     boolean needFocusedWindowUpdate = false;
    299                     synchronized (this) {
    300                         while (!mNeedWindowListUpdate && !mNeedFocusedWindowUpdate) {
    301                             wait();
    302                         }
    303                         if (mNeedWindowListUpdate) {
    304                             mNeedWindowListUpdate = false;
    305                             needWindowListUpdate = true;
    306                         }
    307                         if (mNeedFocusedWindowUpdate) {
    308                             mNeedFocusedWindowUpdate = false;
    309                             needFocusedWindowUpdate = true;
    310                         }
    311                     }
    312                     if (needWindowListUpdate) {
    313                         out.write("LIST UPDATE
    ");
    314                         out.flush();
    315                     }
    316                     if (needFocusedWindowUpdate) {
    317                         out.write("ACTION_FOCUS UPDATE
    ");
    318                         out.flush();
    319                     }
    320                 }
    321             } catch (Exception e) {
    322                 // Ignore
    323             } finally {
    324                 if (out != null) {
    325                     try {
    326                         out.close();
    327                     } catch (IOException e) {
    328                         // Ignore
    329                     }
    330                 }
    331                 mWindowManager.removeWindowChangeListener(this);
    332             }
    333             return true;
    334         }
    335     }
    336 }
    ViewServer.java

    可以看到,ViewServer实现Runnable,接下来看看start的实现:

        boolean start() throws IOException {
            if (mThread != null) {
                return false;
            }
    
            mServer = new ServerSocket(mPort, VIEW_SERVER_MAX_CONNECTIONS, InetAddress.getLocalHost());
            mThread = new Thread(this, "Remote View Server [port=" + mPort + "]");
            mThreadPool = Executors.newFixedThreadPool(VIEW_SERVER_MAX_CONNECTIONS);
            mThread.start();
    
            return true;
        }
    
        public void run() {
            while (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) {
                    Slog.w(LOG_TAG, "Connection error: ", e);
                }
            }
        }

    这个Server启动后,使用之前传进来的端口号(4939)创建个ServerSocket,然后在独立的线程里监听这个端口是否有客户端连接请求,有的话传给ViewServerWorker去处理:

    class ViewServerWorker implements Runnable, WindowManagerService.WindowChangeListener {
            private Socket mClient;
            private boolean mNeedWindowListUpdate;
            private boolean mNeedFocusedWindowUpdate;
    
            public ViewServerWorker(Socket client) {
                mClient = client;
                mNeedWindowListUpdate = false;
                mNeedFocusedWindowUpdate = false;
            }
    
            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 = mWindowManager.viewServerListWindows(mClient);
                    } else if (COMMAND_WINDOW_MANAGER_GET_FOCUS.equalsIgnoreCase(command)) {
                        result = mWindowManager.viewServerGetFocusedWindow(mClient);
                    } else if (COMMAND_WINDOW_MANAGER_AUTOLIST.equalsIgnoreCase(command)) {
                        result = windowManagerAutolistLoop();
                    } else {
                        result = mWindowManager.viewServerWindowCommand(mClient,
                                command, parameters);
                    }
    
                    if (!result) {
                        Slog.w(LOG_TAG, "An error occurred with the command: " + command);
                    }
                } catch(IOException e) {
                    Slog.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();
                        }
                    }
                }
            }
    
            public void windowsChanged() {
                synchronized(this) {
                    mNeedWindowListUpdate = true;
                    notifyAll();
                }
            }
    
            public void focusChanged() {
                synchronized(this) {
                    mNeedFocusedWindowUpdate = true;
                    notifyAll();
                }
            }
    
            private boolean windowManagerAutolistLoop() {
                mWindowManager.addWindowChangeListener(this);
                BufferedWriter out = null;
                try {
                    out = new BufferedWriter(new OutputStreamWriter(mClient.getOutputStream()));
                    while (!Thread.interrupted()) {
                        boolean needWindowListUpdate = false;
                        boolean needFocusedWindowUpdate = false;
                        synchronized (this) {
                            while (!mNeedWindowListUpdate && !mNeedFocusedWindowUpdate) {
                                wait();
                            }
                            if (mNeedWindowListUpdate) {
                                mNeedWindowListUpdate = false;
                                needWindowListUpdate = true;
                            }
                            if (mNeedFocusedWindowUpdate) {
                                mNeedFocusedWindowUpdate = false;
                                needFocusedWindowUpdate = true;
                            }
                        }
                        if (needWindowListUpdate) {
                            out.write("LIST UPDATE
    ");
                            out.flush();
                        }
                        if (needFocusedWindowUpdate) {
                            out.write("ACTION_FOCUS UPDATE
    ");
                            out.flush();
                        }
                    }
                } catch (Exception e) {
                    // Ignore
                } finally {
                    if (out != null) {
                        try {
                            out.close();
                        } catch (IOException e) {
                            // Ignore
                        }
                    }
                    mWindowManager.removeWindowChangeListener(this);
                }
                return true;
            }
        }

    从代码中可以看到,HierarchyView通过Socket向设备发送命令,ViewServerWorker来解析处理命令,并把需要返回的值通过socket再发给HierarchyView。

    至此,HierarchyView的大致原理已经了解,发现只要我们自己创建个ServerSocket,并且监听4939端口,然后模仿ViewServer处理相应命令就可以让设备使用HierarchyView了。

    老外就是用的这个方法。所以我们就不用重复造轮子了

    接下来看看老外的解决方法:

      1 /*
      2  * Copyright (C) 2011 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.server;
     18 
     19 import android.app.Activity;
     20 import android.content.Context;
     21 import android.content.pm.ApplicationInfo;
     22 import android.os.Build;
     23 import android.text.TextUtils;
     24 import android.util.Log;
     25 import android.view.View;
     26 import android.view.ViewDebug;
     27 
     28 import java.io.BufferedReader;
     29 import java.io.BufferedWriter;
     30 import java.io.IOException;
     31 import java.io.InputStreamReader;
     32 import java.io.OutputStream;
     33 import java.io.OutputStreamWriter;
     34 import java.lang.reflect.Method;
     35 import java.net.InetAddress;
     36 import java.net.ServerSocket;
     37 import java.net.Socket;
     38 import java.util.HashMap;
     39 import java.util.List;
     40 import java.util.Map.Entry;
     41 import java.util.concurrent.CopyOnWriteArrayList;
     42 import java.util.concurrent.ExecutorService;
     43 import java.util.concurrent.Executors;
     44 import java.util.concurrent.locks.ReentrantReadWriteLock;
     45 
     46 /**
     47  * <p>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.</p>
     55  * <p/>
     56  * <p>To use this view server, your application must require the INTERNET
     57  * permission.</p>
     58  * <p/>
     59  * <p>The recommended way to use this API is to register activities when
     60  * they are created, and to unregister them when they get destroyed:</p>
     61  * <p/>
     62  * <pre>
     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  * </pre>
     81  * <p/>
     82  * <p>
     83  * In a similar fashion, you can use this API with an InputMethodService:
     84  * </p>
     85  * <p/>
     86  * <pre>
     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  * </pre>
    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<WindowListener> mListeners =
    142             new CopyOnWriteArrayList<WindowListener>();
    143 
    144     private final HashMap<View, String> mWindows = new HashMap<View, String>();
    145     private final ReentrantReadWriteLock mWindowsLock = new ReentrantReadWriteLock();
    146 
    147     private View mFocusedWindow;
    148     private final ReentrantReadWriteLock mFocusLock = new ReentrantReadWriteLock();
    149 
    150     private static ViewServer sServer;
    151 
    152     /**
    153      * Returns a unique instance of the ViewServer. This method should only be
    154      * called from the main thread of your application. The server will have
    155      * the same lifetime as your process.
    156      * <p/>
    157      * If your application does not have the <code>android:debuggable</code>
    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      * @see #start()
    197      */
    198     private ViewServer(int port) {
    199         mPort = port;
    200     }
    201 
    202     /**
    203      * Starts the server.
    204      *
    205      * @return True if the server was successfully created, or false if it already exists.
    206      * @throws java.io.IOException If the server cannot be created.
    207      * @see #stop()
    208      * @see #isRunning()
    209      */
    210     public boolean start() throws IOException {
    211         if (mThread != null) {
    212             return false;
    213         }
    214 
    215         mThread = new Thread(this, "Local View Server [port=" + mPort + "]");
    216         mThreadPool = Executors.newFixedThreadPool(VIEW_SERVER_MAX_CONNECTIONS);
    217         mThread.start();
    218 
    219         return true;
    220     }
    221 
    222     /**
    223      * Stops the server.
    224      *
    225      * @return True if the server was stopped, false if an error occurred or if the
    226      * server wasn't started.
    227      * @see #start()
    228      * @see #isRunning()
    229      */
    230     public boolean stop() {
    231         if (mThread != null) {
    232             mThread.interrupt();
    233             if (mThreadPool != null) {
    234                 try {
    235                     mThreadPool.shutdownNow();
    236                 } catch (SecurityException e) {
    237                     Log.w(LOG_TAG, "Could not stop all view server threads");
    238                 }
    239             }
    240 
    241             mThreadPool = null;
    242             mThread = null;
    243 
    244             try {
    245                 mServer.close();
    246                 mServer = null;
    247                 return true;
    248             } catch (IOException e) {
    249                 Log.w(LOG_TAG, "Could not close the view server");
    250             }
    251         }
    252 
    253         mWindowsLock.writeLock().lock();
    254         try {
    255             mWindows.clear();
    256         } finally {
    257             mWindowsLock.writeLock().unlock();
    258         }
    259 
    260         mFocusLock.writeLock().lock();
    261         try {
    262             mFocusedWindow = null;
    263         } finally {
    264             mFocusLock.writeLock().unlock();
    265         }
    266 
    267         return false;
    268     }
    269 
    270     /**
    271      * Indicates whether the server is currently running.
    272      *
    273      * @return True if the server is running, false otherwise.
    274      * @see #start()
    275      * @see #stop()
    276      */
    277     public boolean isRunning() {
    278         return mThread != null && mThread.isAlive();
    279     }
    280 
    281     /**
    282      * Invoke this method to register a new view hierarchy.
    283      *
    284      * @param activity The activity whose view hierarchy/window to register
    285      * @see #addWindow(android.view.View, String)
    286      * @see #removeWindow(android.app.Activity)
    287      */
    288     public void addWindow(Activity activity) {
    289         String name = activity.getTitle().toString();
    290         if (TextUtils.isEmpty(name)) {
    291             name = activity.getClass().getCanonicalName() +
    292                     "/0x" + System.identityHashCode(activity);
    293         } else {
    294             name += "(" + activity.getClass().getCanonicalName() + ")";
    295         }
    296         addWindow(activity.getWindow().getDecorView(), name);
    297     }
    298 
    299     /**
    300      * Invoke this method to unregister a view hierarchy.
    301      *
    302      * @param activity The activity whose view hierarchy/window to unregister
    303      * @see #addWindow(android.app.Activity)
    304      * @see #removeWindow(android.view.View)
    305      */
    306     public void removeWindow(Activity activity) {
    307         removeWindow(activity.getWindow().getDecorView());
    308     }
    309 
    310     /**
    311      * Invoke this method to register a new view hierarchy.
    312      *
    313      * @param view A view that belongs to the view hierarchy/window to register
    314      * @name name The name of the view hierarchy/window to register
    315      * @see #removeWindow(android.view.View)
    316      */
    317     public void addWindow(View view, String name) {
    318         mWindowsLock.writeLock().lock();
    319         try {
    320             mWindows.put(view.getRootView(), name);
    321         } finally {
    322             mWindowsLock.writeLock().unlock();
    323         }
    324         fireWindowsChangedEvent();
    325     }
    326 
    327     /**
    328      * Invoke this method to unregister a view hierarchy.
    329      *
    330      * @param view A view that belongs to the view hierarchy/window to unregister
    331      * @see #addWindow(android.view.View, String)
    332      */
    333     public void removeWindow(View view) {
    334         mWindowsLock.writeLock().lock();
    335         try {
    336             mWindows.remove(view.getRootView());
    337         } finally {
    338             mWindowsLock.writeLock().unlock();
    339         }
    340         fireWindowsChangedEvent();
    341     }
    342 
    343     /**
    344      * Invoke this method to change the currently focused window.
    345      *
    346      * @param activity The activity whose view hierarchy/window hasfocus,
    347      *                 or null to remove focus
    348      */
    349     public void setFocusedWindow(Activity activity) {
    350         setFocusedWindow(activity.getWindow().getDecorView());
    351     }
    352 
    353     /**
    354      * Invoke this method to change the currently focused window.
    355      *
    356      * @param view A view that belongs to the view hierarchy/window that has focus,
    357      *             or null to remove focus
    358      */
    359     public void setFocusedWindow(View view) {
    360         mFocusLock.writeLock().lock();
    361         try {
    362             mFocusedWindow = view == null ? null : view.getRootView();
    363         } finally {
    364             mFocusLock.writeLock().unlock();
    365         }
    366         fireFocusChangedEvent();
    367     }
    368 
    369     /**
    370      * Main server loop.
    371      */
    372     public void run() {
    373         try {
    374             InetAddress address = InetAddress.getLocalHost();
    375             mServer = new ServerSocket(mPort, VIEW_SERVER_MAX_CONNECTIONS, address);
    376         } catch (Exception e) {
    377             Log.w(LOG_TAG, "Starting ServerSocket error: ", e);
    378         }
    379 
    380         while (mServer != null && Thread.currentThread() == mThread) {
    381             // Any uncaught exception will crash the system process
    382             try {
    383                 Socket client = mServer.accept();
    384                 if (mThreadPool != null) {
    385                     mThreadPool.submit(new ViewServerWorker(client));
    386                 } else {
    387                     try {
    388                         client.close();
    389                     } catch (IOException e) {
    390                         e.printStackTrace();
    391                     }
    392                 }
    393             } catch (Exception e) {
    394                 Log.w(LOG_TAG, "Connection error: ", e);
    395             }
    396         }
    397     }
    398 
    399     private static boolean writeValue(Socket client, String value) {
    400         boolean result;
    401         BufferedWriter out = null;
    402         try {
    403             OutputStream clientStream = client.getOutputStream();
    404             out = new BufferedWriter(new OutputStreamWriter(clientStream), 8 * 1024);
    405             out.write(value);
    406             out.write("
    ");
    407             out.flush();
    408             result = true;
    409         } catch (Exception e) {
    410             result = false;
    411         } finally {
    412             if (out != null) {
    413                 try {
    414                     out.close();
    415                 } catch (IOException e) {
    416                     result = false;
    417                 }
    418             }
    419         }
    420         return result;
    421     }
    422 
    423     private void fireWindowsChangedEvent() {
    424         for (WindowListener listener : mListeners) {
    425             listener.windowsChanged();
    426         }
    427     }
    428 
    429     private void fireFocusChangedEvent() {
    430         for (WindowListener listener : mListeners) {
    431             listener.focusChanged();
    432         }
    433     }
    434 
    435     private void addWindowListener(WindowListener listener) {
    436         if (!mListeners.contains(listener)) {
    437             mListeners.add(listener);
    438         }
    439     }
    440 
    441     private void removeWindowListener(WindowListener listener) {
    442         mListeners.remove(listener);
    443     }
    444 
    445     private interface WindowListener {
    446         void windowsChanged();
    447 
    448         void focusChanged();
    449     }
    450 
    451 
    452     private class ViewServerWorker implements Runnable, WindowListener {
    453         private Socket mClient;
    454         private boolean mNeedWindowListUpdate;
    455         private boolean mNeedFocusedWindowUpdate;
    456 
    457         private final Object[] mLock = new Object[0];
    458 
    459         public ViewServerWorker(Socket client) {
    460             mClient = client;
    461             mNeedWindowListUpdate = false;
    462             mNeedFocusedWindowUpdate = false;
    463         }
    464 
    465         public void run() {
    466             BufferedReader in = null;
    467             try {
    468                 in = new BufferedReader(new InputStreamReader(mClient.getInputStream()), 1024);
    469 
    470                 final String request = in.readLine();
    471 
    472                 Log.i("Command", "===>" + request);
    473 
    474                 String command;
    475                 String parameters;
    476 
    477                 int index = request.indexOf(' ');
    478                 if (index == -1) {
    479                     command = request;
    480                     parameters = "";
    481                 } else {
    482                     command = request.substring(0, index);
    483                     parameters = request.substring(index + 1);
    484                 }
    485 
    486                 boolean result;
    487                 if (COMMAND_PROTOCOL_VERSION.equalsIgnoreCase(command)) {
    488                     result = writeValue(mClient, VALUE_PROTOCOL_VERSION);
    489                 } else if (COMMAND_SERVER_VERSION.equalsIgnoreCase(command)) {
    490                     result = writeValue(mClient, VALUE_SERVER_VERSION);
    491                 } else if (COMMAND_WINDOW_MANAGER_LIST.equalsIgnoreCase(command)) {
    492                     result = listWindows(mClient);
    493                 } else if (COMMAND_WINDOW_MANAGER_GET_FOCUS.equalsIgnoreCase(command)) {
    494                     result = getFocusedWindow(mClient);
    495                 } else if (COMMAND_WINDOW_MANAGER_AUTOLIST.equalsIgnoreCase(command)) {
    496                     result = windowManagerAutolistLoop();
    497                 } else {
    498                     result = windowCommand(mClient, command, parameters);
    499                 }
    500 
    501                 if (!result) {
    502                     Log.w(LOG_TAG, "An error occurred with the command: " + command);
    503                 }
    504             } catch (IOException e) {
    505                 Log.w(LOG_TAG, "Connection error: ", e);
    506             } finally {
    507                 if (in != null) {
    508                     try {
    509                         in.close();
    510 
    511                     } catch (IOException e) {
    512                         e.printStackTrace();
    513                     }
    514                 }
    515                 if (mClient != null) {
    516                     try {
    517                         mClient.close();
    518                     } catch (IOException e) {
    519                         e.printStackTrace();
    520                     }
    521                 }
    522             }
    523         }
    524 
    525         private boolean windowCommand(Socket client, String command, String parameters) {
    526             boolean success = true;
    527             BufferedWriter out = null;
    528 
    529             try {
    530                 // Find the hash code of the window
    531                 int index = parameters.indexOf(' ');
    532                 if (index == -1) {
    533                     index = parameters.length();
    534                 }
    535                 final String code = parameters.substring(0, index);
    536                 int hashCode = (int) Long.parseLong(code, 16);
    537 
    538                 // Extract the command's parameter after the window description
    539                 if (index < parameters.length()) {
    540                     parameters = parameters.substring(index + 1);
    541                 } else {
    542                     parameters = "";
    543                 }
    544 
    545                 final View window = findWindow(hashCode);
    546                 if (window == null) {
    547                     return false;
    548                 }
    549 
    550                 // call stuff
    551                 final Method dispatch = ViewDebug.class.getDeclaredMethod("dispatchCommand",
    552                         View.class, String.class, String.class, OutputStream.class);
    553                 dispatch.setAccessible(true);
    554                 dispatch.invoke(null, window, command, parameters,
    555                         new UncloseableOutputStream(client.getOutputStream()));
    556 
    557                 if (!client.isOutputShutdown()) {
    558                     out = new BufferedWriter(new OutputStreamWriter(client.getOutputStream()));
    559                     out.write("DONE
    ");
    560                     out.flush();
    561                 }
    562 
    563             } catch (Exception e) {
    564                 Log.w(LOG_TAG, "Could not send command " + command +
    565                         " with parameters " + parameters, e);
    566                 success = false;
    567             } finally {
    568                 if (out != null) {
    569                     try {
    570                         out.close();
    571                     } catch (IOException e) {
    572                         success = false;
    573                     }
    574                 }
    575             }
    576 
    577             return success;
    578         }
    579 
    580         private View findWindow(int hashCode) {
    581             if (hashCode == -1) {
    582                 View window = null;
    583                 mWindowsLock.readLock().lock();
    584                 try {
    585                     window = mFocusedWindow;
    586                 } finally {
    587                     mWindowsLock.readLock().unlock();
    588                 }
    589                 return window;
    590             }
    591 
    592 
    593             mWindowsLock.readLock().lock();
    594             try {
    595                 for (Entry<View, String> entry : mWindows.entrySet()) {
    596                     if (System.identityHashCode(entry.getKey()) == hashCode) {
    597                         return entry.getKey();
    598                     }
    599                 }
    600             } finally {
    601                 mWindowsLock.readLock().unlock();
    602             }
    603 
    604             return null;
    605         }
    606 
    607         private boolean listWindows(Socket client) {
    608             boolean result = true;
    609             BufferedWriter out = null;
    610 
    611             try {
    612                 mWindowsLock.readLock().lock();
    613 
    614                 OutputStream clientStream = client.getOutputStream();
    615                 out = new BufferedWriter(new OutputStreamWriter(clientStream), 8 * 1024);
    616 
    617                 for (Entry<View, String> entry : mWindows.entrySet()) {
    618                     out.write(Integer.toHexString(System.identityHashCode(entry.getKey())));
    619                     out.write(' ');
    620                     out.append(entry.getValue());
    621                     out.write('
    ');
    622                 }
    623 
    624                 out.write("DONE.
    ");
    625                 out.flush();
    626             } catch (Exception e) {
    627                 result = false;
    628             } finally {
    629                 mWindowsLock.readLock().unlock();
    630 
    631                 if (out != null) {
    632                     try {
    633                         out.close();
    634                     } catch (IOException e) {
    635                         result = false;
    636                     }
    637                 }
    638             }
    639 
    640             return result;
    641         }
    642 
    643         private boolean getFocusedWindow(Socket client) {
    644             boolean result = true;
    645             String focusName = null;
    646 
    647             BufferedWriter out = null;
    648             try {
    649                 OutputStream clientStream = client.getOutputStream();
    650                 out = new BufferedWriter(new OutputStreamWriter(clientStream), 8 * 1024);
    651 
    652                 View focusedWindow = null;
    653 
    654                 mFocusLock.readLock().lock();
    655                 try {
    656                     focusedWindow = mFocusedWindow;
    657                 } finally {
    658                     mFocusLock.readLock().unlock();
    659                 }
    660 
    661                 if (focusedWindow != null) {
    662                     mWindowsLock.readLock().lock();
    663                     try {
    664                         focusName = mWindows.get(mFocusedWindow);
    665                     } finally {
    666                         mWindowsLock.readLock().unlock();
    667                     }
    668 
    669                     out.write(Integer.toHexString(System.identityHashCode(focusedWindow)));
    670                     out.write(' ');
    671                     out.append(focusName);
    672                 }
    673                 out.write('
    ');
    674                 out.flush();
    675             } catch (Exception e) {
    676                 result = false;
    677             } finally {
    678                 if (out != null) {
    679                     try {
    680                         out.close();
    681                     } catch (IOException e) {
    682                         result = false;
    683                     }
    684                 }
    685             }
    686 
    687             return result;
    688         }
    689 
    690         public void windowsChanged() {
    691             synchronized (mLock) {
    692                 mNeedWindowListUpdate = true;
    693                 mLock.notifyAll();
    694             }
    695         }
    696 
    697         public void focusChanged() {
    698             synchronized (mLock) {
    699                 mNeedFocusedWindowUpdate = true;
    700                 mLock.notifyAll();
    701             }
    702         }
    703 
    704         private boolean windowManagerAutolistLoop() {
    705             addWindowListener(this);
    706             BufferedWriter out = null;
    707             try {
    708                 out = new BufferedWriter(new OutputStreamWriter(mClient.getOutputStream()));
    709                 while (!Thread.interrupted()) {
    710                     boolean needWindowListUpdate = false;
    711                     boolean needFocusedWindowUpdate = false;
    712                     synchronized (mLock) {
    713                         while (!mNeedWindowListUpdate && !mNeedFocusedWindowUpdate) {
    714                             mLock.wait();
    715                         }
    716                         if (mNeedWindowListUpdate) {
    717                             mNeedWindowListUpdate = false;
    718                             needWindowListUpdate = true;
    719                         }
    720                         if (mNeedFocusedWindowUpdate) {
    721                             mNeedFocusedWindowUpdate = false;
    722                             needFocusedWindowUpdate = true;
    723                         }
    724                     }
    725                     if (needWindowListUpdate) {
    726                         out.write("LIST UPDATE
    ");
    727                         out.flush();
    728                     }
    729                     if (needFocusedWindowUpdate) {
    730                         out.write("FOCUS UPDATE
    ");
    731                         out.flush();
    732                     }
    733                 }
    734             } catch (Exception e) {
    735                 Log.w(LOG_TAG, "Connection error: ", e);
    736             } finally {
    737                 if (out != null) {
    738                     try {
    739                         out.close();
    740                     } catch (IOException e) {
    741                         // Ignore
    742                     }
    743                 }
    744                 removeWindowListener(this);
    745             }
    746             return true;
    747         }
    748     }
    749 
    750     private static class UncloseableOutputStream extends OutputStream {
    751         private final OutputStream mStream;
    752 
    753         UncloseableOutputStream(OutputStream stream) {
    754             mStream = stream;
    755         }
    756 
    757         public void close() throws IOException {
    758             // Don't close the stream
    759         }
    760 
    761         public boolean equals(Object o) {
    762             return mStream.equals(o);
    763         }
    764 
    765         public void flush() throws IOException {
    766             mStream.flush();
    767         }
    768 
    769         public int hashCode() {
    770             return mStream.hashCode();
    771         }
    772 
    773         public String toString() {
    774             return mStream.toString();
    775         }
    776 
    777         public void write(byte[] buffer, int offset, int count)
    778                 throws IOException {
    779             mStream.write(buffer, offset, count);
    780         }
    781 
    782         public void write(byte[] buffer) throws IOException {
    783             mStream.write(buffer);
    784         }
    785 
    786         public void write(int oneByte) throws IOException {
    787             mStream.write(oneByte);
    788         }
    789     }
    790 
    791     /**
    792      * 一个空的ViewServer类
    793      */
    794     private static class NoopViewServer extends ViewServer {
    795         private NoopViewServer() {
    796         }
    797 
    798         @Override
    799         public boolean start() throws IOException {
    800             return false;
    801         }
    802 
    803         @Override
    804         public boolean stop() {
    805             return false;
    806         }
    807 
    808         @Override
    809         public boolean isRunning() {
    810             return false;
    811         }
    812 
    813         @Override
    814         public void addWindow(Activity activity) {
    815         }
    816 
    817         @Override
    818         public void removeWindow(Activity activity) {
    819         }
    820 
    821         @Override
    822         public void addWindow(View view, String name) {
    823         }
    824 
    825         @Override
    826         public void removeWindow(View view) {
    827         }
    828 
    829         @Override
    830         public void setFocusedWindow(Activity activity) {
    831         }
    832 
    833         @Override
    834         public void setFocusedWindow(View view) {
    835         }
    836 
    837         @Override
    838         public void run() {
    839         }
    840     }
    841 }
    解决问题的类

    使用方法如下:

    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);
          }
      }

    使用时要注意:app要添加INTERNET权限,并且android:debugable要为true,eclipse或者studio直接run到手机都是debugable的,所以这点不用担心。

    好了,祝大家春节快乐

  • 相关阅读:
    Linq in
    wp7中应用程序清单(WMAppManifest.xml)详细说明
    wp7 给TextBox设置圆角边框
    js 中的闭包
    远程控制PPT软件的帮助
    wp7三种图标大小配置
    在英文版的sqlserver下用LIKE语句不能查询中文
    程序员版《那些年我们一起追过的女孩》(2)
    程序员版《那些年我们一起追过的女孩》(3)
    webbrowser 请求的资源在使用中。 (异常来自 HRESULT:0x800700AA)
  • 原文地址:https://www.cnblogs.com/coding-way/p/4294225.html
Copyright © 2011-2022 走看看