IT_Programming/Android_Java

Android 디바이스의 고유 번호 (Identifier) 획득 시 고려 해야 할 점

JJun ™ 2015. 10. 5. 13:46



 출처

 : http://stunstun.tistory.com/184


 참고자료

 : http://huewu.blog.me/110107222113






안드로이드 개발 시에 디바이스 정보를 획득 하는 일들이 있는데, 그 중 에서도 디바이스의 고유한 번호를 획득 하는 일은 빈번하게 발생 한다. 예를 들면 고유한 디바이스의 정보를 저장 해 어플리케이션의 설치 상태를 확인 한다 던가


다양한 인증 서비스를 제공 할 때 보다 쉽게 서비스를 이용 할 수 있도록 게스트 로그인 같은 기능을 제공 할 때 디바이스에 대한 식별이 필요 할 때가 있다. 하지만 Android SDK 는 API Level 이 업데이트 되면서 이에 대한 정보를 획득 하는 데에 대한 이슈가 있는데 간단하게 나마 정리해보고자 한다.




TelephonyManager


지난 시간 동안 일반적인 경우에는 Hardware 단위로 제공 하는 identifier 를 통해 디바이스의 고유 번호를 획득 하고는 했다. TelephonyManager 클래스를 통해 Phone의 IMEI, MEID 또는 ESN 값을 획득 한다.


예를 들면 아래와 같다.

TelephonyManager manager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);

manager.getDeviceId();


하지만 TelephonyManager  를 통한 방법은 아래와 같은  이슈가 있다.

| Non - Phone : 핸드폰 번호를 갖고 있지 않는 태블릿 이나 Wifi 만을 제공 하는 디바이스는 TelephonyManager 를 통한
                 디바이스 번호를 획득하지 못 할 수도 있다.

| Persistence : 한번 생성된 번호에 대한 지속 여부를 보장 할 수 가 없다. 디바이스가 공장 초기화 될 경우를 예로 들 수 있다.
               뿐만 아니라, 현재 까지 몇몇 버그들이 report 되어 왔다. 안드로이드 디바이스는 제조사 마다 다양하게 커스터마이징 하기 때문에 000000000000000 같이 의미 없는 값이나 null 이 반환 되기도 한다.

| Privilege : 디바이스에 접근 하기 위해  READ_PHONE_STATE  권한 설정이 필요 하다.


Google API에서는 위 메소드(
getDeviceId())를 이렇게 설명하고 있지만 실제로 USIM이 존재 하더라도 디바이스 ID 값이 넘어오지 않는
경우가 있다. (갤럭시 S 초창기 모델 및 몇몇 기억도 안나는 LG 디바이스들..) 그리고 반대의 경우도 있다. (유심을 뺐는데도 디바이스 ID 값이 넘어오는 현상)



Mac address

Wifi 나 blue-tooth 를 사용 하는 디바이스에서 획득 가능 하다. 하지만 디바이스의 고유 번호를 획득 하기 위한 용도로는 추천 되지 않는다. Wifi 가 항상 켜져 있다고 보장 할 수 없을 뿐만 아니라 모든 기기가 고유번호를 반환 하는 것이 아니기 때문이다.





Serial Number

안드로이드의 API Level 9 (진저브레드 2.3) 이후 부터 제공 하는 고유 번호로서 TelephonyManager 클래스를 통한 획득 보다는 안전 하다고 할 수 있지만 2.3 미만의 Version 에서는 문제가 발생 할 수 가 있다.

API Level 9 부터 제공 하기 때문에 @SuppressLint("NewApi") 를 추가 해야 되기 때문에 아래와 같이 Java reflection을 통해 획득 하는 것이 좋다.

private static String getDeviceSerialNumber() {

 try {

   return (String) Build.class.getField("SERIAL").get(null);

 } catch (Exception ignored) {

   return null;

 }

}





ANDROID_ID

가장 명확한 방법이며, 디바이스가 최초 Boot 될때 생성 되는 64-bit 값이다. ANDROID_ID는 디바이스의 고유한 번호를 획득 하기 위해 가장 좋은 선택이 될 수 있다.

Settings.Secure.ANDROID_ID

하지만 ANDROID_ID 또한 단점이 있는데, Proyo 2.2 이전 Version 에는 100% 디바이스 고유 번호를 획득 한다고는 보장 할 수 없으며 몇몇 Vendor 에서 출하된 디바이스에 동일한 고유 번호가 획득 된다는 버그가 report 됐다는 것이다.



결론

지금 까지 안드로이드 플랫폼에서 디바이스를 구별하기 위한 고유한 번호를 획득 하는 방법을 알아 보았는데, 물리적으로 100% 보장 할 수 없다는 것이고, 이로 인해 결코 쉽지 않은 일이라는 것을 알 수 있었다. 가장 좋은 방법은 ANDROID_ID를 통해 획득 하는 방법이며, 다른 해결책을 혼용 해 사용하는 것도 좋을 방법 일 것이다. 여기에 위에 나열된 예상 되는 이슈 들과 같은 만일의 사태에 대한 대비책을 만들어 놓는 것도 좋을 것 이다.


참조 : http://android-developers.blogspot.kr/2011/03/identifying-app-installations.html#uds-search-results

더보기

public class Installation {

    private static String sID = null;

    private static final String INSTALLATION = "INSTALLATION";


    public synchronized static String id(Context context) {

        if (sID == null) {  

            File installation = new File(context.getFilesDir(), INSTALLATION);

            try {

                if (!installation.exists())

                    writeInstallationFile(installation);

                sID = readInstallationFile(installation);

            } catch (Exception e) {

                throw new RuntimeException(e);

            }

        }

        return sID;

    }


    private static String readInstallationFile(File installation) throws IOException {

        RandomAccessFile f = new RandomAccessFile(installation, "r");

        byte[] bytes = new byte[(int) f.length()];

        f.readFully(bytes);

        f.close();

        return new String(bytes);

    }


    private static void writeInstallationFile(File installation) throws IOException {

        FileOutputStream out = new FileOutputStream(installation);

        String id = UUID.randomUUID().toString();

        out.write(id.getBytes());

        out.close();

    }

}





※ 유니크한 디바이스 아이디값 만들기



1) UUID 사용

   : 프로그램 재 설치시 값이 달라진다. (http://stackoverflow.com/questions/5088474/how-can-i-get-the-uuid-of-my-android-phone-in-an-application)


 public class Installation {

    private static String sID = null;

    private static final String INSTALLATION = "INSTALLATION";


    public synchronized static String id(Context context) {

        if (sID == null) {  

            File installation = new File(context.getFilesDir(), INSTALLATION);

            try {

                if (!installation.exists())

                    writeInstallationFile(installation);

                sID = readInstallationFile(installation);

            } catch (Exception e) {

                throw new RuntimeException(e);

            }

        }

        return sID;

    }


    private static String readInstallationFile(File installation) throws IOException {

        RandomAccessFile f = new RandomAccessFile(installation, "r");

        byte[] bytes = new byte[(int) f.length()];

        f.readFully(bytes);

        f.close();

        return new String(bytes);

    }


    private static void writeInstallationFile(File installation) throws IOException {

        FileOutputStream out = new FileOutputStream(installation);

        String id = UUID.randomUUID().toString();

        out.write(id.getBytes());

        out.close();

    }

 }




 import android.content.Context;

 import android.content.SharedPreferences;
 import android.provider.Settings.Secure;
 import android.telephony.TelephonyManager;
 import java.io.UnsupportedEncodingException;
 import java.util.UUID;

 

 public class DeviceUuidFactory { 
      protected static final String PREFS_FILE = "device_id.xml"; 

      protected static final String PREFS_DEVICE_ID = "device_id"; 
      // protected static UUID uuid; 

      public static String getDeviceUuid(Context context) { 
            UUID uuid = null;

            if( uuid ==null ) { 

                  synchronized (DeviceUuidFactory.class) { 

                        if( uuid == null) { 

                              final SharedPreferences prefs = context.getSharedPreferences( PREFS_FILE, 0);

                              final String id = prefs.getString(PREFS_DEVICE_ID, null );

                              if (id != null) { 

                                    // Use the ids previously computed and stored in the prefs file 

                                    uuid = UUID.fromString(id); 

                              } else { 

                                    final String androidId = Secure.getString(context.getContentResolver(), Secure.ANDROID_ID);

                                    // Use the Android ID unless it's broken, in which case fallback on deviceId, 

                                    // unless it's not available, then fallback on a random number which we store 

                                    // to a prefs file 

                                    try { 

                                          if (!"9774d56d682e549c".equals(androidId)) {

                                                uuid = UUID.nameUUIDFromBytes(androidId.getBytes("utf8"));

                                          } else { 

                                                final String deviceId = 

                                                      ((TelephonyManager) context.getSystemService( Context.TELEPHONY_SERVICE )).getDeviceId();

                                                uuid = deviceId!=null ? UUID.nameUUIDFromBytes(deviceId.getBytes("utf8")) : UUID.randomUUID();

                                          

                                    } catch (UnsupportedEncodingException e) {

                                          throw new RuntimeException(e);

                                    

                                    // Write the value out to the prefs file 

                                    prefs.edit().putString(PREFS_DEVICE_ID, uuid.toString() ).commit();

                              

                        

                  

            


            return uuid.toString();

      
 /** 
  ** Returns a unique UUID for the current android device. 
  * As with all UUIDs, this unique ID is "very highly likely" 
  ** to be unique across all Android devices. Much more so than ANDROID_ID is. 
  *** The UUID is generated by using ANDROID_ID as the base key if appropriate, falling back on 
  ** TelephonyManager.getDeviceID() if ANDROID_ID is known to be incorrect, and finally falling back 
  ** on a random UUID that's persisted to SharedPreferences if getDeviceID() does not return a 
  ** usable value. 
  ** 
  ** In some rare circumstances, this ID may change. In particular, if the device is factory reset a new device ID
  ** may be generated. 
  * In addition, if a user upgrades their phone from certain buggy implementations of Android 2.2 
  ** to a newer, non-buggy version of Android, the device ID may change. 
  * Or, if a user uninstalls your app on 
  ** a device that has neither a proper Android ID nor a Device ID, this ID may change on reinstallation. 
  ** 
  ** Note that if the code falls back on using TelephonyManager.getDeviceId(), the resulting ID will NOT 
  ** change after a factory reset. Something to be aware of. 
  ** 
  ** Works around a bug in Android 2.2 for many devices when using ANDROID_ID directly. 
  ** 
  ** @see http://code.google.com/p/android/issues/detail?id=10603 
  ** 
  ** @return a UUID that may be used to uniquely identify your device for most purposes. 
  **/ 
 }




// 카카오 SDK 에서 사용했던(?) 메서드 (바로 위의 코드와 동일한 내용이다.)

/*

  1. 먼저 가장 정확하다고 알려진 ANDROID_ID 를 가져옵니다.
  2. 하지만 예전 어떤 기기에서 특정 번호로만 나오던 버그가 있었다고 합니다. 이에 대한 예외처리를 해줍니다.
  3. ANDROID_ID 가 없으면 getDeviceID() 를 사용합니다.
  4. 둘 다 실패하면, 랜덤하게 UUID 를 발생시킵니다.
  5. 4번은 랜덤한 값이지만 preference 에 저장해두고 사용하기 때문에 삭제하고 재설치하지 않는 이상 랜덤한 넘버를 두번 만들지는 않습니다.

*/ 

public static String getDeviceUUID(final Context context) {

    final SharedPreferencesCache cache = Session.getAppCache();

    final String id = cache.getString(PROPERTY_DEVICE_ID);

  

    UUID uuid = null;

    if (id != null) {

        uuid = UUID.fromString(id);

    } else {

        final String androidId = Secure.getString(context.getContentResolver(), Secure.ANDROID_ID);

        try {

            if (!"9774d56d682e549c".equals(androidId)) {

                uuid = UUID.nameUUIDFromBytes(androidId.getBytes("utf8"));

            } else {

                final String deviceId = ((TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE)).getDeviceId();

                uuid = deviceId != null ? UUID.nameUUIDFromBytes(deviceId.getBytes("utf8")) : UUID.randomUUID();

            }

        } catch (UnsupportedEncodingException e) {

            throw new RuntimeException(e);

        }

  

        Bundle bundle = new Bundle();

        bundle.putString(PROPERTY_DEVICE_ID, uuid.toString());

        cache.save(bundle);

    }

  

    return uuid.toString();

}





2) 혼용해서 사용하기


예1)

 

 // Null 체크 생량 - 예외처리 구현할 것!

 final TelephonyManager tm = (TelephonyManager) getBaseContext().getSystemService(Context.TELEPHONY_SERVICE);

 

 final String tmDevice, tmSerial, androidId;

 tmDevice = "" + tm.getDeviceId();

 tmSerial = "" + tm.getSimSerialNumber();

 androidId = "" + android.provider.Settings.Secure.getString(getContentResolver(), android.provider.Settings.Secure.ANDROID_ID);

 

 UUID deviceUuid = new UUID(androidId.hashCode(), ((long)tmDevice.hashCode() << 32) | tmSerial.hashCode());

 String deviceId = deviceUuid.toString();



예2) Pseudo-Unique ID  (http://www.pocketmagic.net/?p=1662)


 // 이것은 확률적으로 같은 하드웨어와 같은 ROM IMAGE를 쓰지 않으면 동일 아이디가 발생 하지 않을 꺼라 하는데 쫌 찜찜하긴 하네요 ㅎ

 // 결국 종합적으로 따져본 결과 DEVICEID + PHONE NUMBER + MACADDRESS을 OR로 사용하기로 함.

 return "35" + // we make this look like a valid IMEI
         Build.BOARD.length()%10+ Build.BRAND.length()%10 +
        Build.CPU_ABI.length()%10 + Build.DEVICE.length()%10 +
        Build.DISPLAY.length()%10 + Build.HOST.length()%10 +
        Build.ID.length()%10 + Build.MANUFACTURER.length()%10 +
        Build.MODEL.length()%10 + Build.PRODUCT.length()%10 +
        Build.TAGS.length()%10 + Build.TYPE.length()%10 +
        Build.USER.length()%10 ; //13 digits



예3)

 String m_szLongID = m_szImei + m_szDevIDShort + m_szAndroidID+ m_szWLANMAC + m_szBTMAC;

 // compute md5

 MessageDigest m = null;

 try { 

m = MessageDigest.getInstance("MD5");

 } catch (NoSuchAlgorithmException e) {

e.printStackTrace();

 } 


 m.update(m_szLongID.getBytes(),0,m_szLongID.length());

 // get md5 bytes

 byte p_md5Data[] = m.digest();

 // create a hex string

 String m_szUniqueID = new String();

 for (int i=0;i<p_md5Data.length;i++) {

int b =  (0xFF & p_md5Data[i]);

// if it is a single digit, make sure it have 0 in front (proper padding)

if (b <= 0xF) { 

m_szUniqueID += "0";

}

// add number to string

m_szUniqueID+=Integer.toHexString(b); 

 }


 // hex string to uppercase

 m_szUniqueID = m_szUniqueID.toUpperCase();





3) 버전 분기처리해서 사용하기

예) 2.2 이전 버전으로는 TelephonyManager.getDeviceId(); 를 통해서 가져오고 2.2 이후 버전에서는 Settings.Secure.ANDROID_ID 로 가져오는
    방법을 사용



4) OpenUDID 라이브러리 사용


- iOS / MacOS code: https://github.com/ylechelle/OpenUDID - Android code: https://github.com/vieux/OpenUDID



(참고자료 1) 안드로이드 개별 디바이스를 번역하는 방법 (안드로이드 공식 블로그의 번역) 
                 http://huewu.blog.me/110107222113

(참고자료 2) IMEI, MEID, ESN, IMSI 등에 대해 자세히 나옴 
                http://www.webs.co.kr/index.php?mid=adnroid&sort_index=readed_count&order_type=asc&page=2&document_srl=38470

(참고자료 3) Stack Overflow 의 방대한 문답들 
                http://stackoverflow.com/questions/2785485/is-there-a-unique-android-device-id

(참고자료 4) 카카오톡 안드로이드 SDK 
                 https://developers.kakao.com/docs/android