Undetected USB accessory with Arduino Due connected to Android MiniPad (MPgio)

0

I have problem working on an Android accessory job with Arduino Due which is uploaded Blink.ino (other samples too). The problem is that I couldn't detect any USB accessory (or List) from UsbManager got from getSystemService("USB_SERVICE") in any case with attaching or detaching.

My code is below.

1. manifest

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.signalhello"
    android:versionCode="1"
    android:versionName="1.0" >

    <!--  <uses-feature android:name="android.hardware.usb.accessory" />
    <uses-feature android:name="android.hardware.usb.host" /> -->

    <uses-sdk
        android:minSdkVersion="8"
        android:targetSdkVersion="17" />

    <application
        android:allowBackup="true"
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme" >
        <activity
            android:name="com.example.signalhello.MainActivity"
            android:label="@string/app_name" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
            <intent-filter>
                <action android:name="android.hardware.usb.action.USB_ACCESSORY_ATTACHED" />
                <action android:name="android.hardware.usb.action.USB_ACCESSORY_DETACHED" />
            </intent-filter>
            <meta-data
                android:name="android.hardware.usb.action.USB_ACCESSORY_ATTACHED"
                android:resource="@xml/accessory_filter" />
        </activity>
    </application>
</manifest>



2. accessory-filter.xml in res/xml/

<?xml version="1.0" encoding="utf-8"?>
<resources>
        <usb-accessory manufacturer="Arduino-er"/> // I tried other options like <usb-    accessory vendor-id="9025" product-id="62"/>
    <usb-device vendor-id="9025" product-id="62"/>
    <usb-accessory vendor-id="2341" product-id="003e"/>
    <usb-device vendor-id="2341" product-id="003e"/>
</resources>



3. MainActivity

package com.example.signalhello;

import java.util.HashMap;
import java.util.Iterator;
import java.util.Set;

import android.annotation.TargetApi;
import android.app.Activity;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.hardware.usb.UsbAccessory;
import android.hardware.usb.UsbDevice;
import android.hardware.usb.UsbInterface;
import android.hardware.usb.UsbManager;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.util.Log;
import android.view.Menu;
import android.widget.TextView;

@TargetApi(Build.VERSION_CODES.HONEYCOMB_MR1)
public class MainActivity extends Activity {

    private static final String ACTION_USB_PERMISSION =
        "net.hardroid.adk.example.action.USB_PERMISSION";
    private PendingIntent mPermissionIntent;
    private boolean mPermissionRequestPending;

    // USB가 감지되었을 때의 이벤트를 받음.
    private final BroadcastReceiver mUsbReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            String action = intent.getAction();
            Log.d("onReceive", "action: " + action);
            tv01.setText("action: " + action);
            if (ACTION_USB_PERMISSION.equals(action)) {
                // 사용자에게 Android Accessory Protocol을 구현한 장비가 연결되면
                // 수락할 것인지 문의한 다이얼로그에 대한 사용자의 선택 결과를 받는다.
                synchronized (this) {
                    Log.d("onReceive", "Getting UsbAccessory...");
                    UsbAccessory accessory = intent
                            .getParcelableExtra(UsbManager.EXTRA_ACCESSORY);
                    UsbDevice usbDevice = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
                    Log.d("onReceive", "Accessory is null ? " + (accessory == null) + ", Device is null ? " + (usbDevice == null));
                    if (intent.getBooleanExtra(
                            UsbManager.EXTRA_PERMISSION_GRANTED, false)) {
                        Log.d("onReceive", "Connected USB ...");
                        // 수락했을 경우
                        showMessage("receiver : USB Host 연결됨.");
                    }
                    else {
                        Log.d("onReceive", "Permission denied....");
                        Log.d(MainActivity.class.getName(),
                                "permission denied for accessory " + accessory);
                        showMessage("permission denied.");
                    }

                    openAccessory(accessory);

                    UsbManager manager = null;
                    manager = (UsbManager) getSystemService(Context.USB_SERVICE);

                    HashMap<String, UsbDevice> deviceList = manager
                            .getDeviceList();
                    Iterator<UsbDevice> deviceIter = deviceList.values()
                            .iterator();

                    while (deviceIter.hasNext()) {
                        UsbDevice device = deviceIter.next();

                        // Device's class...
                        int count = device.getInterfaceCount();
                        tv01.setText("count:" + count);
                        for (int i = 0; i < count; i++) {
                            UsbInterface iface = device.getInterface(i);
                            tv02.setText(tv02.getText() + "," + iface.getId());
                        }
                    }
                    // 연결 수락 결과를 받았음을 표시
                    mPermissionRequestPending = false;
                    tv04.setText("3");
                }
            }
            else
                if (UsbManager.ACTION_USB_ACCESSORY_DETACHED.equals(action)) {
                    // Android Accessory Protocol을 구현한 장비의 연결이 해제되었을 때
                    UsbAccessory accessory = intent
                            .getParcelableExtra(UsbManager.EXTRA_ACCESSORY);
                    // 앱이 사용하고 있는 장비와 같은 것인지 확인
                    if (accessory != null && accessory.equals(mAccessory)) {
                        showMessage("USB Host   연결 해제됨.");
                        closeAccessory();
                    }
                    tv04.setText("4");

            }
            tv04.setText("5");
        }
    };

    private TextView tv01;
    private TextView tv02;
    private TextView tv03;
    private TextView tv04;
    /*static class IncomingHandler extends Handler {
        private final WeakReference<UDPListenerService> mService;

        IncomingHandler(UDPListenerService service) {
            mService = new WeakReference<UDPListenerService>(service);
        }
        @Override
        public void handleMessage(Message msg)
        {
             UDPListenerService service = mService.get();
             if (service != null) {
                  service.handleMessage(msg);
             }
        }
    }*/

    /*Handler mIncomingHandler = new Handler(new Handler.Callback() {
        @Override
        public boolean handleMessage(Message msg) {
        }
    });*/
    static Handler uiHandler = new Handler() {

        @Override
        public void handleMessage(Message msg) {
            Log.d("handleMessage", "msg.what: " + msg.what + ", msg.obj: " + msg.obj);
            switch (msg.what) {
            case 1:
            }
        }
    };

    private UsbAccessory mAccessory;
    private AdkHandler handler;
    private static UsbManager mUsbManager;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        tv01 = (TextView) findViewById(R.id.tv01);
        tv02 = (TextView) findViewById(R.id.tv02);
        tv03 = (TextView) findViewById(R.id.tv03);
        tv04 = (TextView) findViewById(R.id.tv04);
        mUsbManager = (UsbManager) getSystemService(Context.USB_SERVICE);
        Log.d("onCreate", "Permission intent getting...");
        mPermissionIntent = PendingIntent.getBroadcast(this, 0, new Intent(
        ACTION_USB_PERMISSION), 0);
        IntentFilter filter = new IntentFilter(ACTION_USB_PERMISSION);
        filter.addAction(UsbManager.ACTION_USB_ACCESSORY_DETACHED);
        filter.addAction(UsbManager.ACTION_USB_ACCESSORY_ATTACHED);
        Log.d("onCreate", "registering Receiver..");
        Log.d("onCreate", "receiver is null ? " + (mUsbReceiver == null));

        registerReceiver(mUsbReceiver, filter);
        Log.d("onCreate", "registered Receiver..");
        Log.d("onCreate", "Got permission intent...");

        tv04.setText("1");
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        // Inflate the menu; this adds items to the action bar if it is present.
        getMenuInflater().inflate(R.menu.main, menu);
        return true;
    }
    /** 액티비티가 화면에 보일 때 호출 */
    @Override
    public void onResume() {
        super.onResume();
        Log.d("onResume", "Getting device list");
        // 앱이 화면에 보일 때 안드로이드 장비에 Android Accessory Protocol을
        // 구현한 USB Host가 연결되어 있는지 확인
        HashMap<String, UsbDevice> devices = mUsbManager.getDeviceList();
        Log.d("onResume", "Getting UsbAccessory");
        UsbAccessory accessory = getIntent().getParcelableExtra(UsbManager.EXTRA_ACCESSORY);
        if (accessory != null) { // Android Accessory Protocol를 구현한 장비를 찾았을 경우
            Log.d("onResume", "Got accessory");
            if (mUsbManager.hasPermission(accessory)) {
                Log.d("onResume", "Got permission");
                showMessage("onresume : USB Accessory 연결됨.");
                openAccessory(accessory);
            }
            else {
                synchronized (mUsbReceiver) {
                    if (!mPermissionRequestPending) {
                        mUsbManager.requestPermission(accessory,
                                mPermissionIntent); // USB 연결을 통해 장비에 연결해도 되는지 사용자에게 문의
                        mPermissionRequestPending = true; // 연결권한을 물어보드 코드를 실행했음을 표시
                    }
                }
            }
        }
        else {
            String device = "";
            if (devices != null) {
                Set<String> keys = devices.keySet();
                for (String key : keys) {
                    device += key + ": " + devices.get(key).getDeviceName();
                }
            }

            showMessage("mAccessory is null, devices: " + (devices == null ? "null" : device));
            Log.d(MainActivity.class.getName(), "mAccessory is null");
        }
        tv04.setText("2");
    }

    // 액티비티가 소멸될 때 호출
    @Override
    protected void onDestroy() {
        // 브로드캐스트 리시버를 제거
        unregisterReceiver(mUsbReceiver);
        super.onDestroy();
    }
    private void showMessage(String msg){
        Log.d("showMessage", msg);
        tv03.setText("message: " + msg);
    }
    private void openAccessory(UsbAccessory accessory){
        mAccessory = accessory;
        if(handler == null){
            Log.d("openAccessory", "Instanciating AdkHandler");
            handler = new AdkHandler();
            Log.d("openAccessory", "Setting UI Handler to AdkHandler");
            handler.setUiHandler(uiHandler);
        }
        Log.d("openAccessory", "Openning AdkHandler");
        handler.open(mUsbManager, mAccessory);
    }

    private void closeAccessory(){
        if(handler != null && handler.isConnected())
            handler.close();
        mAccessory = null;
    }
}



3-1.AdkHandler.java

package com.example.signalhello;

import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.FileOutputStream;

import android.annotation.TargetApi;
import android.hardware.usb.UsbAccessory;
import android.hardware.usb.UsbManager;
import android.os.Build;
import android.os.Handler;
import android.os.Message;
import android.os.ParcelFileDescriptor;
import android.util.Log;

/**
 *
 * @author Chun, Young-yil. at IBS Inc.
 * @since ADK 4.0
 * @date 2013. 5. 15.
 */
@TargetApi(Build.VERSION_CODES.HONEYCOMB_MR1)
public class AdkHandler implements Runnable {

    private ParcelFileDescriptor mFileDescriptor;
    private FileInputStream mInputStream;
    private FileOutputStream mOutputStream;
    private Handler uiHandler;

    private boolean running;

    /* (non-Javadoc)
     * @see java.lang.Runnable#run()
     */
    @Override
    public void run() {
        Log.d("run", "start");
        int ret = 0;
        byte[] buffer = new byte[16384];
        int i;

        while (ret >= 0 && running) {
            try {
                ret = mInputStream.read(buffer);
                Log.d("run", "" + ret);
            }
            catch (Exception e) {
                e.printStackTrace();
                break;
            }

            i = 0;
            Log.d("run", "i: " + i  + ", ret: " + ret);
            while ( i < ret && running) {
                int len = ret - i;
                Log.d("run", "buffer[" + i + "]: " + buffer[i]);
                switch (buffer[i]) {
                    case 0x1:
                        if (len >= 3) {
                            Message m = Message.obtain(uiHandler, 1);
                            int value = composeInt(buffer[i = 1], buffer[i + 2]);
                            m.obj = value;
                            uiHandler.sendMessage(m);
                            Log.d("value", "" + value);
                        }
                        i += 3;
                        break;

                    default :
                        Log.d("default", "unknown msg:" + buffer[i]);
                        i = len;
                        break;
                }
            }
        }
    }

    /**
     * @param b
     * @param c
     * @return
     */
    private int composeInt(byte high, byte low) {
        int val = (int) high & 0xff;
        val *= 256;
        val += (int) low & 0xff;
        return val;
    }

    public void open(UsbManager usbManager, UsbAccessory accessory) {
        Log.d("open", "Getting FileDescriptor");
        mFileDescriptor = usbManager.openAccessory(accessory);
        if (mFileDescriptor == null) {
            Log.d("open", "Failed to get ParcelFileDescriptor");
            Log.d("보드연결", "실패");
            Message msg = Message.obtain(uiHandler, -1);
            msg.obj = "보드연결 실패";
            Log.d("open", "Sending fail message to UI Handler...");
            uiHandler.sendMessage(msg);
        }
        else
        {
            Log.d("open", "Succeeded in getting ParcelFileDescriptor");
            Log.d("open", "Getting FileDescriptor from ParcelFileDescriptor");
            FileDescriptor fd = mFileDescriptor.getFileDescriptor();
            Log.d("open", "Getting FileInputStream...");
            mInputStream = new FileInputStream(fd);

            Log.d("open", "New Threading..");
            Thread thread = new Thread(null, this, "ADK Example");
            running = true;
            Log.d("open", "Starting thread...");
            thread.start();
            Log.d("보드연결", "성공");
        }
    }

    public void close() {
        Log.d("close", "Closing...");
        running = false;
    }

    /**
     * 멤버 uiHandler 을 회수
     * @return the uiHandler
     */
    public Handler getUiHandler() {
        return uiHandler;
    }

    /**
     * sets the uiHandler of this instance to given uiHandler.
     * 멤버속성 uiHandler 을 인수 uiHandler 으로 세팅.
     * @param uiHandler the uiHandler to set
     */
    public void setUiHandler(Handler uiHandler) {
        this.uiHandler = uiHandler;
    }

    /**
     * @return
     */
    public boolean isConnected() {
        return (mInputStream != null && mOutputStream != null);
    }
}



_4. acivity_main.xml_

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context=".MainActivity" >

    <LinearLayout
        android:id="@+id/ll01"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentLeft="true"
        android:layout_alignParentTop="true"
        android:orientation="vertical" >

        <TextView
            android:id="@+id/tv01"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="장비미인식" />

        <TextView
            android:id="@+id/tv02"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="-1" />

        <TextView
            android:id="@+id/tv03"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="-1" />

        <TextView
            android:id="@+id/tv04"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="-1" />
    </LinearLayout>

</RelativeLayout>



5. logcat ( using adbwireless in rooting MPgio minipad )

  • 05-31 12:16:12.728: I/wpa_supplicant(400): [CTRL_IFACE]SIGNAL_POLL
  • 05-31 12:16:15.298: W/PowerUI(207): unknown intent: Intent { act=android.intent.action.ACTION_POWER_CONNECTED flg=0x10000010 }
  • 05-31 12:16:15.488: I/WindowManager(140): MediaPlayer.is not PlayingVideo
  • 05-31 12:16:15.728: I/wpa_supplicant(400): [CTRL_IFACE]SIGNAL_POLL
  • 05-31 12:16:16.088: I/WindowManager(140): MediaPlayer.is not PlayingVideo
  • 05-31 12:16:17.218: I/USB3G(86): event { 'add', '/devices/platform/sw_hcd_host0/usb1/1-1', 'usb', '', 189, 15 }
  • 05-31 12:16:17.218: I/USB3G(86): path : '/sys/devices/platform/sw_hcd_host0/usb1/1-1'
  • 05-31 12:16:17.218: I/USB3G(86): VID :size 5,vid_path '/sys/devices/platform/sw_hcd_host0/usb1/1-1/idVendor',VID '2341
  • 05-31 12:16:17.218: I/USB3G(86): '.
  • 05-31 12:16:17.218: I/USB3G(86): PID :size 5,Pid_path '/sys/devices/platform/sw_hcd_host0/usb1/1-1/idProduct',PID '003e
  • 05-31 12:16:17.218: I/USB3G(86): '.
  • 05-31 12:16:17.218: I/USB3G(86): cmd=/system/etc/usb_modeswitch.sh /system/etc/usb_modeswitch.d/2341_003e &,
  • 05-31 12:16:17.228: E/EventHub(140): could not get driver version for /dev/input/mouse0, Not a typewriter
  • 05-31 12:16:17.228: D/EventHub(140): No input device configuration file found for device 'Arduino LLC Arduino Due '.
  • 05-31 12:16:17.248: I/EventHub(140): New device: id=15, fd=247, path='/dev/input/event4', name='Arduino LLC Arduino Due ', classes=0x8000000b, configuration='', keyLayout='/system/usr/keylayout/Generic.kl', keyCharacterMap='/system/usr/keychars/Generic.kcm', builtinKeyboard=false
  • 05-31 12:16:17.298: I/USB3G(86): excute ret : 0,err:No such file or directory
  • 05-31 12:16:17.318: I/InputReader(140): Device added: id=15, name='Arduino LLC Arduino Due ', sources=0x00002103
  • 05-31 12:16:17.318: I/ActivityManager(140): Config changed: {1.3 0mcc0mnc ko_KR layoutdir=0 sw640dp w1066dp h592dp lrg land finger qwerty/v/v -nav/h s.33}
  • 05-31 12:16:17.338: D/OpenGLRenderer(1631): Flushing caches (mode 0)
  • 05-31 12:16:17.478: D/Activity(1631): pckname = com.example.signalhello mComponent = com.example.signalhello.MainActivity
  • 05-31 12:16:17.538: I/StatusBar.HeightReceiver(207): Resizing status bar plugged=false height=36 old=36
  • 05-31 12:16:17.658: D/onCreate(1631): Permission intent getting...
  • 05-31 12:16:17.658: D/onCreate(1631): registering Receiver..
  • 05-31 12:16:17.668: D/onCreate(1631): receiver is null ? false
  • 05-31 12:16:17.668: D/onCreate(1631): registered Receiver..
  • 05-31 12:16:17.668: D/onCreate(1631): Got permission intent...
  • 05-31 12:16:17.698: D/onResume(1631): Getting device list
  • 05-31 12:16:17.698: D/onResume(1631): Getting UsbAccessory
  • 05-31 12:16:17.698: D/showMessage(1631): mAccessory is null, devices:
  • 05-31 12:16:17.698: D/com.example.signalhello.MainActivity(1631): mAccessory is null
  • 05-31 12:16:17.728: D/ViewRootImpl(1631): pckname = com.example.signalhello
  • 05-31 12:16:18.138: I/WindowManager(140): MediaPlayer.is not PlayingVideo
  • 05-31 12:16:18.738: I/wpa_supplicant(400): [CTRL_IFACE]SIGNAL_POLL
  • 05-31 12:16:20.068: D/dalvikvm(140): GC_EXPLICIT freed 267K, 24% free 8736K/11399K, paused 8ms+13ms
  • 05-31 12:16:21.738: I/wpa_supplicant(400): [CTRL_IFACE]SIGNAL_POLL

I'm very confused with my situation with above things.

The Arduino board is connected with my notebook PC (programming port). (I think it provides power to the board, doesn't it?) Because the power supply is needed, no other power supplier I have.

And, I compiled Blink.ino which I downloaded from Arduino's site. (I have tested other sketches. But no news.)

And, the board works blinking when uploaded by the Arduino code writing tool. But when I attached (connected) the board by USB cable to Android device MPgio MiniPad, I have no sign of detection with my codes above.

Just system_process issues some strange messages (which are included above logcat messages) like '05-31 12:16:17.228: E/EventHub(140): could not get driver version for /dev/input/mouse0, Not a typewriter' or '05-31 12:16:17.228: D/EventHub(140): No input device configuration file found for device 'Arduino LLC Arduino Due '.

But, just below those, it prints 'New device: id......' which is from system_process's EventHub Tag.

I want to just detect my Arduino board.

android
usb
arduino
accessory
asked on Stack Overflow May 31, 2013 by zennzero1 • edited Jun 1, 2013 by Peter Mortensen

3 Answers

0

I have searched to solve this problem many sites, blogs etc. And finally I became to have a doubt about that the hardware - MPgio - support this kind of application. Because I found some blogs say all kinds of android device necessarily do not support this. So I called to MPgio customer center to let me contact with technical supervisor or consultant who could answer whether the device supports the ADK - USB Accessory, Host application.

Having talked with another man, I was told to get a call later by another man who has an answer for that. And I was called when my way to home. And he had asked some questions about my requests, but he couldn't answer by right that time, instead he promised to call me again with answer for this. And finally! I got a call of who made a promise, and he said the device does not support this. And it was kernel problem.

And he added they have a case to install the driver -FTDI? I heard- for distribution of which meets specific client requirements (with payment included, I heard).

... And I am heard that if I need that I must pay or rent - 'lease' I may resume the way he said - this on which is replied that I am not the man who could answer this right the time...

That's all.

But I might have to add some for other developers who might have problems similar to this.

First of all, the Blink.ino is not the good case which has not the code of declaration of ADK instance that might be the protocol which can interface to other supporting device. Actually, I have tested other codes which have such an announcement. But it didn't make a success anyway - in my case, the device not supported.

And, I have to write a minute thing that the arduino 1.5.2 uses ADK library which replaced the AndroidAccessory(.h) library being used in earlier version - whiche version I cannot point out specifically.

In some blogs I followed the way of making codes, and imported the AndroidAccessory library , compiled to failure. So I went further, and another blogs I found the case 1.5.2 uses the ADK - the site is 'http://arduino-er.blogspot.kr/2013/03/hello-world-adk-communication-between.html'. Actually this sites is the final case I am referencing for my hello codes now.

But that sure failed to work in success - in my case, the device problem I mentioned. If other things happen about this which have to be posted, I will write those again.

Thank you all who has made an effort to solve this anyway...

answered on Stack Overflow Jun 5, 2013 by zennzero1 • edited Jun 5, 2013 by zennzero1
0

After my first own answering, I had to know about the devices which might be compatible with ADK 2012, I asked about that to Arduino google group -https://groups.google.com/a/arduino.cc/group/developers/browse_thread/thread/955aa9342270c2a2/8e03022da7aeab3f#8e03022da7aeab3f - and to another Korean site named 'Hardroid' - http://hardroid.net, and was replied with messages which he wasn't sure that he could name the compatible devices specifically. But he was much sure that the reference phones and tablets from Google Nexus series would compatible with ADK which might be adopted in Arduino Due I'm using.

answered on Stack Overflow Jun 7, 2013 by zennzero1 • edited Jun 7, 2013 by zennzero1
0

The result is this. We, my company has decided to change the model of tablet, and the board. What surprised me is that the ADK.h in newer version of Arduino board does not work with new tablet working on Jelly Bean (Android SDK 4.2.2) while AndroidAccessory.h does work. I almost omitted the fact we changed the board from Due to Mega_ADK. I heard that the Due does not work with ADK properly from my colleage who contacted the vendor in Korea, but actually I can't believe this because I had seen the U-tube file working Due with device I don't remember the model (probably HTC's).

I'd finished USB-Accessory job with newer tablet ASUS Memo-pad with success. I don't know the exact reason of not working with ADK.h in sketch right now.

answered on Stack Overflow Jun 25, 2013 by zennzero1 • edited Jun 25, 2013 by zennzero1

User contributions licensed under CC BY-SA 3.0