IT_Programming/Android_Java

[안드로이드] 폰에서 자기 전화번호 불러오는 방법 / Marshmallow READ_PHONE_STATE 권한 관련 이슈

JJun ™ 2014. 11. 16. 19:07



 출처http://i5on9i.blogspot.kr/2015/10/blog-post_70.html


 한마디만 하자면 "개판이다."! 

 구글은 새로운 것도 좋지만 자신들이 싼 똥들을 잘 치울 필요가 있다고 생각한다.
 (물론 많이 수정도 됐지만.. 제조사, 통신사 커스텀에 대한 적정 수준의 검수/통제가 필요하다고 생각한다.)

 제조사와 통신사 똥까지 합쳐져서 앱 개발자들은 똥사태의 피해자가 될 판이다.



전화번호 가져오기

사실 정확하게는 MSISDN 값을 가져온다고 해야 할 듯 하다. 왜냐하면, GSM 에서는 MSISDN 을 전화번호로 사용하는 듯 하지만
, 북미에서 예전에는 MIN 을 전화번호로 사용했다고 한다.


getLine1Number 사용

Programmatically obtain the phone number of the Android phone - Stack Overflow 
: 전화번호를 가져온다. 그런데 댓글들을 확인해 보면, 문제는 SIM 에 전화번호가 보통 기록되어 있지 않을 수 있다고 한다.
그래서 TelephonyManager 를 이용하는 것은 완전하지 않은 듯 보인다.


android - How to get phone number from an incoming call? - Stack Overflow 
: 하지만 이처럼 incoming call 에서 전화번호를 얻어오는 것은 훨씬 가능한 일이다.



getLine1Number로 전화번호를 못가져오는 경우들

Android: getLine1Number로 전화번호가 읽어지지 않는경우? - 앱개발 질문 - 안드로이드 개발 질문/답변
getLine1Number로 전화번호가 읽어지지 않는경우 : 네이버 블로그



연락처 가져오기 방법




IMSI 와 MSISDN

전화번호를 가져오는 것과 관련해서 IMSI 와 MSISDN 에 대한 개념이 좀 더 분명해 져야 했다. 그래서 좀 더 알아봤다.
  • IMP's Blog: 이동통신 각종 식별번호 개념잡기(IMSI, MSISDN, MIN등) 
    : 이 글에서 MSISDN 부분을 보면 "전화번호" 가 무엇을 이야기 하는 지 알려준다.

  • IMSI Vs MSISDN 
    : MSISDN 이 SIM 에 없다고 이야기 해준다. 우리가 SIM을 잃어버려도 다시 SIM 만 구입해도 원래의 번호를
      사용할 수 있는 것도 이 덕분이다. IMSI 는 우리가 전화기 전원을 켜서 통신사 기지국에 대한 사용을 허가받을 때 쓴다.
      이녀석은 우리가 웹페이지등에 login 을 할 때 쓰는 id 같은 존재라고 보면 된다.
      이 IMSI + MSISDN 을 이용해서 고유한 ID 를 형성하는 듯 하다.

  • IT는 이제 삶속에.. :: IMEI, IMSI, MSISDN 단말식별자 
    : 여기서도 IMSI 는 SIM 에 unique 하게 들어있지만, MSISDN 은 그렇지 않다고 한다.
      보통 MSISDN 이 call 을 routing 하는 데 쓰인다고 한다.




Phone number 관련 library



See Also






결론

일단 MSISDN 이 SIM 에 없는 경우도 있다는 것을 알았다.
그러나 기본적으로 이 방법 말고는 딱히 바로 가져올 수 있는 법이 없어 보인다.
(그래서 일부 글에서는 확실한 방법은 직접 입력하도록 하는 것이라고 한다.)

일단 이것을 가져오도록 해 놓고, edittext 등을 통해 수정할 수 있도록 해 놓는 것도 방법이 될 수 있겠다.

소스를 보면, 여러가지 방식으로 시도하는 듯 하다. 대부분의 경우 SDK 가 번호를 가져올 수 있다고 보는게 맞을 지도 모르겠다. (추측)

public String getLine1NumberForSubscriber(int subId) { String number = null; try { ITelephony telephony = getITelephony(); if (telephony != null) number = telephony.getLine1NumberForDisplay(subId, mContext.getOpPackageName()); } catch (RemoteException ex) { } catch (NullPointerException ex) { } if (number != null) { return number; } try { IPhoneSubInfo info = getSubscriberInfo(); if (info == null) return null; return info.getLine1NumberForSubscriber(subId, mContext.getOpPackageName()); } catch (RemoteException ex) { return null; } catch (NullPointerException ex) { // This could happen before phone restarts due to crashing return null; } }







안드로이드 6(Marshmallow) 에서 READ_PHONE_STATE 권한 관련 이슈



보통 아래처럼 전화번호를 가져오게 되는데, Android 6에서 갑자기 문제가 발생했다.


TelephonyManager tm = (TelephonyManager)mAppContext.getSystemService(Context.TELEPHONY_SERVICE);
String mPhoneNumber = tm.getLine1Number();

갑자기 잘 동작하던 것이 SecurityException 을 발생시켰다.

이 때문에 혹시 permission 요청을 해야하나 하고, 요청을 해봤는데도 안됐다. 이미 permission 은 GRANTED 였다.

if (checkSelfPermission(READ_PHONE_STATE) == PackageManager.PERMISSION_GRANTED) { return true; }



issue 2938

그래서 한참을 찾다가 결국 bug 인 것을 알았다.


아직까지 방법은 targetSdkVersion 의 버전을 낮춰놓아야 하는 듯 하다.

android {
    compileSdkVersion 23
    buildToolsVersion "22.0.1"
    defaultConfig {
        applicationId "com.ccccc"
        minSdkVersion 14
        targetSdkVersion 22
        versionCode 1
        versionName "1.0"











1. 버튼등 onClickListener등에 넣어 연락처 선택 화면이 나오게 합니다.

 

 Intent intent = new Intent(Intent.ACTION_PICK);

 intent.setData(ContactsContract.CommonDataKinds.Phone.CONTENT_URI);
         startActivityForResult(intent, 0);




2. 연락처 선택을 하면 결과값을 받아옵니다.


  @Override

  protected void onActivityResult(int requestCode, int resultCode, Intent data) {
     if (resultCode == RESULT_OK)
     {
        Cursor cursor = getContentResolver().query(data.getData(), 
              new String[]{ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME
                    ContactsContract.CommonDataKinds.Phone.NUMBER}, null, null, null);
        cursor.moveToFirst();
        name = cursor.getString(0);        // 0은 이름을 얻어옵니다.
        number = cursor.getString(1);   // 1은 번호를 받아옵니다.
            cursor.close();
     }
     super.onActivityResult(requestCode, resultCode, data);
  } 




 [참고 1]


 private ArrayList<Contact> getContactList() {


Uri uri = ContactsContract.CommonDataKinds.Phone.CONTENT_URI;

String[] projection = new String[] {
ContactsContract.CommonDataKinds.Phone.CONTACT_ID, // 연락처 ID -> 사진 정보 가져오는데 사용
ContactsContract.CommonDataKinds.Phone.NUMBER,        // 연락처
ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME }; // 연락처 이름.

String[] selectionArgs = null;

String sortOrder = ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME
+ " COLLATE LOCALIZED ASC";

Cursor contactCursor = managedQuery(uri, projection, null,
selectionArgs, sortOrder);

ArrayList<Contact> contactlist = new ArrayList<Contact>();

if (contactCursor.moveToFirst()) {
do {
String phonenumber = contactCursor.getString(1).replaceAll("-",
"");
if (phonenumber.length() == 10) {
phonenumber = phonenumber.substring(0, 3) + "-"
+ phonenumber.substring(3, 6) + "-"
+ phonenumber.substring(6);
} else if (phonenumber.length() > 8) {
phonenumber = phonenumber.substring(0, 3) + "-"
+ phonenumber.substring(3, 7) + "-"
+ phonenumber.substring(7);
}

Contact acontact = new Contact();
acontact.setPhotoid(contactCursor.getLong(0));
acontact.setPhonenum(phonenumber);
acontact.setName(contactCursor.getString(2));

contactlist.add(acontact);
} while (contactCursor.moveToNext());
}

return contactlist;

  }


 

ContactsSample.zip





 [참고 2]


 needMobPhone을 사용할 경우 전화번호 기준으로 중복된 사람들을 필터하도록 했습니다.

이걸 이용할 만한 소스를 당장 만들일이 없기 때문에 이쯤에서 close합니다.

  1. // 패키지 이름은 직접 알맞게 써주세요.
  2.  
  3. import java.util.ArrayList;
  4. import java.util.HashMap;
  5. import java.util.regex.Pattern;
  6.  
  7. import android.content.Context;
  8. import android.database.Cursor;
  9. import android.provider.ContactsContract;
  10. import android.provider.ContactsContract.CommonDataKinds.Email;
  11. import android.provider.ContactsContract.CommonDataKinds.Event;
  12. import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
  13. import android.provider.ContactsContract.CommonDataKinds.Note;
  14. import android.provider.ContactsContract.CommonDataKinds.Organization;
  15. import android.provider.ContactsContract.CommonDataKinds.Phone;
  16. import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
  17. import android.provider.ContactsContract.CommonDataKinds.Website;
  18. import android.provider.ContactsContract.Contacts;
  19. import android.provider.ContactsContract.Data;
  20. import android.provider.ContactsContract.Groups;
  21. import android.util.Log;
  22.  
  23. /**
  24. * 2015-05-19 전명 박용서 작성<br/>
  25. * 빠르게 기초만 만들어둔거라... 소스정리가 엉망이네요.<br/>
  26. * 나중에 정리좀해서 국내용인 전화코드 국제용으로 처리하고 뫼 라이브러리에 넣어서 배포할 생각입니다.
  27. * 2015-05-26 전명 박용서 : needMobPhone 사용시 휴대전화번호 중복 사용자 필터
  28. * 2015-06-23 전명 박용서 : 그룹추가
  29. */
  30. public class PhonebookReader
  31. {
  32. final private Context ctx;
  33. private boolean needMobPhone = false;
  34. /** 중복또한 제거한다. [null이나 빈값을 포함할경우 중복을 제거하면 기준이 애매해지기 때문에 이걸 사용할 때만 제거한다.] */
  35. public void needMobPhone() { needMobPhone = true; }
  36. private boolean useEmail = false;
  37. public void useEmail() { useEmail = true; }
  38. private boolean useAddr = false;
  39. public void useAddr() { useAddr = true; }
  40. private boolean useMemo = false;
  41. public void useMemo() { useMemo = true; }
  42. private boolean useCompany = false;
  43. public void useCompany() { useCompany = true; }
  44. private boolean useDuty = false;
  45. public void useDuty() { useDuty = true; }
  46. private boolean useHomepage = false;
  47. public void useHomepage() { useHomepage = true; }
  48. private boolean useBirthday = false;
  49. public void useBirthday() { useBirthday = true; }
  50. private boolean useGroup = false;
  51. public void useGroup() { useGroup = true; }
  52. public void useAll()
  53. {
  54. useEmail = useAddr = useMemo = useCompany = useDuty = useHomepage = useBirthday = useGroup = true;
  55. }
  56. public PhonebookReader(Context ctx)
  57. {
  58. this.ctx = ctx;
  59. }
  60. public ArrayList<Member> getMemberList()
  61. {
  62. ArrayList<Member> list = new ArrayList<Member>();
  63. HashMap<Integer, Member> map = new HashMap<Integer, Member>();
  64. Member node = null;
  65. int lastId = -1;
  66. HashMap<Long, String> groupList = useGroup ? getGroupList() : null;
  67. Cursor c = ctx.getContentResolver().query
  68. (
  69. ContactsContract.CommonDataKinds.Phone.CONTENT_URI, null, null, null, Data.CONTACT_ID
  70. );
  71. // 아이디
  72. int c_id = c.getColumnIndex(Data.CONTACT_ID);
  73. int c_type = c.getColumnIndex(Phone.TYPE);
  74. final int c_name = c.getColumnIndex(Contacts.DISPLAY_NAME);
  75. final int p_number = c.getColumnIndex(Phone.NUMBER);
  76. while (c.moveToNext())
  77. {
  78. // 아이디
  79. int id = c.getInt(c_id);
  80. int type = c.getInt(c_type);
  81. // 신규작성
  82. if (id != lastId)
  83. {
  84. lastId = id;
  85. list.add((node = new Member()));
  86. map.put(id, node);
  87. node.id = id;
  88. node.name = c.getString(c_name);
  89. }
  90. String number = filtLinePhone(c.getString(p_number));
  91. if (number != null)
  92. {
  93. if (isMobPhone(number))
  94. {
  95. if (node.mobilePhone == null)
  96. {
  97. node.mobilePhone = number;
  98. }
  99. }
  100. else
  101. {
  102. switch (type)
  103. {
  104. // 집전화
  105. case Phone.TYPE_HOME :
  106. node.homePhone = number;
  107. break;
  108. // 회사전화
  109. case Phone.TYPE_COMPANY_MAIN : case Phone.TYPE_WORK :
  110. node.officePhone = number;
  111. break;
  112. // 팩스
  113. case Phone.TYPE_FAX_HOME : case Phone.TYPE_FAX_WORK : case Phone.TYPE_OTHER_FAX :
  114. node.fax = number;
  115. break;
  116. }
  117. }
  118. }
  119. }
  120. c.close();
  121. if (needMobPhone)
  122. {
  123. HashMap<String, Character> dup = new HashMap<String, Character>();
  124. String hp;
  125. for (int i = 0 ; i < list.size() ; )
  126. {
  127. hp = list.get(i).mobilePhone;
  128. if (hp != null && dup.get(hp) == null)
  129. {
  130. dup.put(hp, '1');
  131. i++;
  132. }
  133. else
  134. {
  135. list.remove(i);
  136. }
  137. }
  138. }
  139. c = ctx.getContentResolver().query ( ContactsContract.Data.CONTENT_URI, null, null, null, Data.CONTACT_ID );
  140. c_id = c.getColumnIndex(Data.CONTACT_ID);
  141. c_type = c.getColumnIndex(StructuredPostal.TYPE);
  142. int c_mime = c.getColumnIndex(Data.MIMETYPE);
  143. int c_mail = c.getColumnIndex(Email.DATA);
  144. int c_memo = c.getColumnIndex(Note.NOTE);
  145. int c_com = c.getColumnIndex(Organization.COMPANY);
  146. int c_duty = c.getColumnIndex(Organization.TITLE);
  147. int c_site = c.getColumnIndex(Website.URL);
  148. int c_bd = c.getColumnIndex(Event.START_DATE);
  149. int c_haddr = c.getColumnIndex(StructuredPostal.FORMATTED_ADDRESS);
  150. int c_hazip = c.getColumnIndex(StructuredPostal.POBOX);
  151. int c_oaddr = c.getColumnIndex(StructuredPostal.FORMATTED_ADDRESS);
  152. int c_oazip = c.getColumnIndex(StructuredPostal.POBOX);
  153. int c_groupno = c.getColumnIndex(GroupMembership.GROUP_ROW_ID);
  154. lastId = -1; // 초기화
  155. String val;
  156. while (c.moveToNext())
  157. {
  158. int id = c.getInt(c_id);
  159. // 아이디가 달라지면
  160. if (id != lastId)
  161. {
  162. lastId = id;
  163. node = map.get(c.getInt(c_id));
  164. }
  165. // 없는정보
  166. if (node == null)
  167. {
  168. continue;
  169. }
  170. // 마임
  171. String mime = c.getString(c_mime);
  172. if (mime.equals(Email.CONTENT_ITEM_TYPE))
  173. {
  174. if (useEmail && isEmail(val = c.getString(c_mail)))
  175. {
  176. node.email = val;
  177. }
  178. }
  179. else if (mime.equals(Note.CONTENT_ITEM_TYPE))
  180. {
  181. if (useMemo && (val = c.getString(c_memo)) != null)
  182. {
  183. node.memo = val;
  184. }
  185. }
  186. else if (mime.equals(Organization.CONTENT_ITEM_TYPE))
  187. {
  188. if (useCompany && (val = c.getString(c_com)) != null)
  189. {
  190. node.company = val;
  191. }
  192. if (useDuty && (val = c.getString(c_duty)) != null)
  193. {
  194. node.duty = val;
  195. }
  196. }
  197. else if (mime.equals(Website.CONTENT_ITEM_TYPE))
  198. {
  199. if (useHomepage && c.getType(c_type) == Website.TYPE_HOMEPAGE && (val = c.getString(c_site)) != null)
  200. {
  201. node.homepage = val;
  202. }
  203. }
  204. else if (mime.equals(Event.CONTENT_ITEM_TYPE))
  205. {
  206. if (useBirthday && c.getType(c_type) == Event.TYPE_BIRTHDAY && (val = c.getString(c_bd)) != null)
  207. {
  208. node.birthday = val;
  209. }
  210. }
  211. else if (mime.equals(StructuredPostal.CONTENT_ITEM_TYPE))
  212. {
  213. if (useAddr)
  214. {
  215. switch (c.getType(c_type))
  216. {
  217. case StructuredPostal.TYPE_HOME :
  218. if ((val = c.getString(c_haddr)) != null)
  219. {
  220. node.homeAddr = val;
  221. }
  222. if ((val = c.getString(c_hazip)) != null)
  223. {
  224. node.homeZip = val;
  225. }
  226. break;
  227. case StructuredPostal.TYPE_WORK :
  228. if ((val = c.getString(c_oaddr)) != null)
  229. {
  230. node.officeAddr = val;
  231. }
  232. if ((val = c.getString(c_oazip)) != null)
  233. {
  234. node.officeZip = val;
  235. }
  236. break;
  237. }
  238. }
  239. }
  240. else if (mime.equals(GroupMembership.CONTENT_ITEM_TYPE))
  241. {
  242. // 그룹이 발견될 경우 더 이상 찾지 않기 위해.
  243. if (useGroup && node.groupNo == 0)
  244. {
  245. node.groupName = groupList.get((node.groupNo = c.getLong(c_groupno)));
  246. }
  247. }
  248. }
  249. c.close();
  250. return list;
  251. }
  252. /**
  253. * 그룹을 가져온다.
  254. */
  255. public HashMap<Long, String> getGroupList()
  256. {
  257. HashMap<Long, String> groupList = new HashMap<Long, String>();
  258. Cursor c = ctx.getContentResolver().query
  259. (
  260. Groups.CONTENT_SUMMARY_URI, new String[] { Groups._ID, Groups.TITLE }, null, null, null
  261. );
  262. int c_id = c.getColumnIndex(Groups._ID);
  263. int c_name = c.getColumnIndex(Groups.TITLE);
  264. while (c.moveToNext())
  265. {
  266. groupList.put(c.getLong(c_id), c.getString(c_name));
  267. }
  268. c.close();
  269. return groupList;
  270. }
  271. final private static Pattern PAT_COUNTRY_CODE_KOREA = Pattern.compile("^(\\+|\\-)?82\\-?");
  272. final private static Pattern PAT_PHONE_NOP = Pattern.compile("^[\\d]{9,11}$");
  273. final private static Pattern PAT_PHONE = Pattern.compile("^[\\d]{2,3}\\-[\\d]{3,4}\\-[\\d]{4}$");
  274. private String filtLinePhone(String no)
  275. {
  276. if (no == null || no.length() == 0) { return null; }
  277. // 국가번호를 제거하고 정규화함.
  278. if (!(no = PAT_COUNTRY_CODE_KOREA.matcher(no).replaceFirst("")).startsWith("0"))
  279. {
  280. no = '0' + no;
  281. }
  282. // 하이픈이 없는 경우 [정규화]
  283. if (no.indexOf('-') == -1)
  284. {
  285. switch (no.length())
  286. {
  287. case 9 :
  288. no = no.substring(0, 2) + '-' + no.substring(2, 5) + '-' + no.substring(5);
  289. break;
  290. case 10 :
  291. no = no.startsWith("02")
  292. ? (no.substring(0, 2) + '-' + no.substring(2, 6) + '-' + no.substring(6))
  293. : (no.substring(0, 3) + '-' + no.substring(3, 6) + '-' + no.substring(6));
  294. break;
  295. case 11 :
  296. no = no.substring(0, 3) + '-' + no.substring(3, 7) + '-' + no.substring(7);
  297. break;
  298. default : return null;
  299. }
  300. return no;
  301. }
  302. if (PAT_PHONE.matcher(no).matches() && no.length() >= 11 && no.length() <= 13)
  303. {
  304. return no;
  305. }
  306. return null;
  307. }
  308. final private static Pattern PAT_MOB_PHONE = Pattern.compile("^01(0|1|6|7|8|9)\\-[\\d\\-]+");
  309. /** 주의 먼저 filtLinePhone에 통과된것을 사용하자 */
  310. public static boolean isMobPhone(String no)
  311. {
  312. return PAT_MOB_PHONE.matcher(no).matches();
  313. }
  314. final public static Pattern PAT_MAIL = Pattern.compile("^[\\._a-z0-9\\-]+@[\\._a-z0-9\\-]+\\.[a-z]{2,}$");
  315. private boolean isEmail(String email)
  316. {
  317. return email != null && PAT_MAIL.matcher(email).matches();
  318. }
  319. public static class Member
  320. {
  321. public int id;
  322. public String name;
  323. public String mobilePhone;
  324. public String homePhone;
  325. public String officePhone;
  326. public String fax;
  327. public String homeAddr;
  328. public String homeZip;
  329. public String officeAddr;
  330. public String officeZip;
  331. public String email;
  332. public String memo;
  333. public String company;
  334. public String duty;
  335. public String homepage;
  336. public String birthday;
  337. public long groupNo = 0;
  338. public String groupName;
  339. public void log(String tag)
  340. {
  341. // 로그용인데 스트링빌더 필요없겠지...
  342. String text = "[" + id + "] " + name;
  343. Log.d(tag, text);
  344. text = "- " +
  345. mobilePhone + ", " + homePhone + ", " + officePhone + ", " + fax + ", " +
  346. homeAddr + ", " + homeZip + ", " + officeAddr + ", " + officeZip + ", " +
  347. email + ", " + memo + ", " + company + ", " + duty + ", " + homepage + ", " + birthday + ", " + groupName;
  348. Log.d(tag, text);
  349. }
  350. }
  351. }




ContactsSample.zip
0.62MB
ContactsSample.zip
0.62MB