앱을 하나 만들고 있다. geoFences 을 이용해서 내가 지금 어디에 있는가를 보고 자동으로 설정을 하거나 알림을 하는 앱을 ... 그런데, 난관(?)이 하나 생겼다. 집을 나오면 wifi을 끄고, 다시 집에 오면 wifi 을 자동으로 켜는 기능을 넣고 싶었는데, 내가 지금 쓰는 폰이 API가 29 (Android 10) 이상이라는 상황 때문에 wifimanager 의 기능중에 setWifiEnabled 을 사용할 수 없다는 것이다.
#스하리1000명프로젝트, Nawala sa Korea? Kahit na hindi ka nagsasalita ng Korean, tinutulungan ka ng app na ito na madaling makalibot. Sabihin lang ang iyong wika—ito ay nagsasalin, naghahanap, at nagpapakita ng mga resulta pabalik sa iyong wika. Mahusay para sa mga manlalakbay! Sinusuportahan ang 10+ wika kabilang ang English, Japanese, Chinese, Vietnamese, at higit pa. Subukan ito ngayon! https://play.google.com/store/apps/details?id=com.billcoreatech.opdgang1127
또한 아래 예제에서 보여 주고 있는 것 처럼 adapter 을 구현하는 소스의 모양도 조금 다르니 구조를 보고 이해를 해 두는 것이 좋을 것 같다. listview 을 활용하는 경우의 adapter 구현을 모양과 비교를 해 보면 거의 비슷해 보이기는 하나, ViewHolder 을 기본으로 구현해 주고 있으니 잘 활용할 수 있으면 좋을 것 같다. ViewHolder 을 구현하는 이유는 자원 재활용에 도움이 된다고 했던 것으로 본 기억이 있다. list 가 스크롤 되면서 화면 밖으로 넘어가면 없어지고 그 자리를 다른 데이터로 채우는 모양으로 자원을 재활용 한다는 설명을 어딘가에서 보았던 기억이 난다.
public class SimpleTextAdapter extends RecyclerView.Adapter<SimpleTextAdapter.ViewHolder> {
private ArrayList<String> mData = null ;
// 아이템 뷰를 저장하는 뷰홀더 클래스. public class ViewHolder extends RecyclerView.ViewHolder { TextView textView1 ;
ViewHolder(View itemView) { super(itemView) ;
// 뷰 객체에 대한 참조. (hold strong reference) textView1 = itemView.findViewById(R.id.text1) ; } }
// 생성자에서 데이터 리스트 객체를 전달받음. public SimpleTextAdapter(ArrayList<String> list) { mData = list ; }
// onCreateViewHolder() - 아이템 뷰를 위한 뷰홀더 객체 생성하여 리턴. @Override public SimpleTextAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { Context context = parent.getContext() ; LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) ;
// onBindViewHolder() - position에 해당하는 데이터를 뷰홀더의 아이템뷰에 표시. @Override public void onBindViewHolder(SimpleTextAdapter.ViewHolder holder, int position) { String text = mData.get(position) ; holder.textView1.setText(text) ; }
// getItemCount() - 전체 데이터 갯수 리턴. @Override public int getItemCount() { return mData.size() ; } }
@Override public int getCount() { return chattingListArrayList.size(); }
@Override public ChatRooms getItem(int position) { return chattingListArrayList.get(position); }
@Override public long getItemId(int position) { return position; }
public void updateReceiptsList(ArrayList<ChatRooms> _oData) { chattingListArrayList = _oData; nListCnt = chattingListArrayList.size(); // 배열 사이즈 다시 확인 this.notifyDataSetChanged(); // 그냥 여기서 하자 }
Button crashButton = new Button(this); crashButton.setText("Test Crash"); crashButton.setOnClickListener(new View.OnClickListener() { public void onClick(View view) { throw new RuntimeException("Test Crash"); // Force a crash } });
addContentView(crashButton, new ViewGroup.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
이건 뭐할 떄 쓰면 좋은가 하는 생각이 든다. 앱을 만들고 나서 누군가에게 전달을 했는데, 그 앱에서 에러가 난다고 한다. 흐미... 내가 테스트 할 때는 문제가 없었던 것 같은데 ...
그래서 이런 저런 방법을 찾아보던 중에 오호라... 이런 것도 있네... 각설하고... 일단 개발자 가이드를 읽어보자. 불행하게도 지금(2021.08.07일)까지는 한글로 된 문서가 안드로이드에 적합하게 되어 있지 않은 것 같다. 어쩔 수 없어 영문 사이트를 보면서 따라하기...(크롬의 자동번역기능을 이용해서)
내용은 같은 내용이지만, 아래 링크는 안드로이드에 대한 설며이 없고, 위에 링크는 설명은 있지만, 영문 페이지이고, 한글은 지원 하지 않는다. 아직 한글 사용자가 많지 않아서 인지... 흠흠흠...
뭐 하여간 크롬이 지원하는 자동번역기능을 이용해서 살펴보면...
위 그림 처럼 firebase console 에서 Crashlytics 을 들어가 보면 모래시계가 나오고 계속해서 기다린다고 되어 있다. 그래서 설명을 읽고 Crashlytics 을 활성화 하고 그 다음 할일은 gradle 을 수정하는 것이다...
먼저 project 의 gradle 파일에 아래 classpath 을 추가한다.
dependencies { ...
// Add the Crashlytics Gradle plugin classpath 'com.google.firebase:firebase-crashlytics-gradle:2.7.1' } }
다음은 apps 의 gradle 파일에 추가한다.
apply plugin: 'com.google.firebase.crashlytics'
...
dependencies { // Import the BoM for the Firebase platform implementation platform('com.google.firebase:firebase-bom:28.3.0')
// Declare the dependencies for the Crashlytics and Analytics libraries // When using the BoM, you don't specify versions in Firebase library dependencies implementation 'com.google.firebase:firebase-crashlytics' implementation 'com.google.firebase:firebase-analytics' }
그리고 나서 gradle 파일에서 sync 을 눌러서 필요한 파일들을 준비하고 나면 그다음은 마지막으로 MainActivity 에 아래 코드를 넣고 실행을 한번 하는 것이다.
Button crashButton = new Button(this); crashButton.setText("Test Crash"); crashButton.setOnClickListener(new View.OnClickListener() { public void onClick(View view) { throw new RuntimeException("Test Crash"); // Force a crash } });
addContentView(crashButton, new ViewGroup.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
이 코드는 고민할 것도 없다. MainActivity 의 onCreate 에 넣고 실행하면 화면에 내가 design 하지 않은 버튼이 하나 나온다. Test Crash 라고 ... 그럼 그냥 한번 눌러주면 Firebase 의 console 화면이 변한다.. 아래 그림 처럼
그러면 이제 준비가 끝난 것이다. 그럼 이제 MainActivity 에 넣어던 위에 Test Crase 버튼은 필요가 없다. comment 처리를 하고 내가 만든 앱을 실행해 보는 것이다. 그러면 혹시나 모르는 오류가 발생했을 떄, console 에서 리스트를 확인해 볼 수 있다. 원격지에 있는 사람이 사용하다가 오류를 발생시키더라도 그의 폰에서 로그를 받아올 필요가 없어지는 것이다.
다음은 gradle 파일에 retrofit 사용을 위해서 implementation 을 선언한다. 나는 데이터를 json 방식으로 받아서 처리를 할 것이라서 simplexml 을 없어도 되나. 데이터를 혹시나 xml 구조로 받아야 하는 경우가 있을 때는 기술할 필요가 있다.
이제 카카오개발자 페이지에서 새로 앱을 만들기 등록을 하고, API 키를 받는다...(저 그림의 키는 일부이니 붙여넣기를 해도 소용이 없을 듯...)
그리고 플랫폼에 android 앱에 대한 정보를 하나 등록을 해야 하고, 키 해시는 앱을 실행해서 나오는 키 해시 값을 등록해 주어야 카카오 지도를 활용할 수 있다.
public PackageInfo getPackageInfo(final Context context, int flag) { try { return context.getPackageManager().getPackageInfo(context.getPackageName(), flag); } catch (PackageManager.NameNotFoundException e) { Log.w(TAG, "Unable to get PackageInfo", e); } return null; }
public String getKeyHash(Context context) { PackageInfo packageInfo = getPackageInfo(context, PackageManager.GET_SIGNATURES); if (packageInfo == null) return null;
public class ResponseBean { @SerializedName("meta") Meta meta ; @SerializedName("documents") ArrayList<Documents> documents ;
public Meta getMeta() { return meta; }
public ArrayList<Documents> getDocuments() { return documents; }
public void setMeta(Meta meta) { this.meta = meta; }
public void setDocuments(ArrayList<Documents> documents) { this.documents = documents; }
public class Meta { @SerializedName("same_name") SameName sameName ; @SerializedName("pageable_count") int pageableCount ; @SerializedName("total_count") int totalCount ; @SerializedName("is_end") boolean isEnd ;
public SameName getSameName() { return sameName; }
public int getPageableCount() { return pageableCount; }
public int getTotalCount() { return totalCount; }
public boolean isEnd() { return isEnd; }
public void setSameName(SameName sameName) { this.sameName = sameName; }
public void setPageableCount(int pageableCount) { this.pageableCount = pageableCount; }
public void setTotalCount(int totalCount) { this.totalCount = totalCount; }
public void setEnd(boolean end) { isEnd = end; } }
public String getAddressName() { return addressName; }
public String getCategoryGroupCode() { return categoryGroupCode; }
public String getCategoryGroupName() { return categoryGroupName; }
public String getCategoryName() { return categoryName; }
public String getDistance() { return distance; }
public String getId() { return id; }
public String getPhone() { return phone; }
public String getPlaceName() { return placeName; }
public String getPlaceUrl() { return placeUrl; }
public double getPosX() { return Double.parseDouble(posX); }
public double getPosY() { return Double.parseDouble(posY); }
public String getRoadAddressName() { return roadAddressName; }
public void setAddressName(String addressName) { this.addressName = addressName; }
public void setCategoryGroupCode(String categoryGroupCode) { this.categoryGroupCode = categoryGroupCode; }
public void setCategoryGroupName(String categoryGroupName) { this.categoryGroupName = categoryGroupName; }
public void setCategoryName(String categoryName) { this.categoryName = categoryName; }
public void setDistance(String distance) { this.distance = distance; }
public void setId(String id) { this.id = id; }
public void setPhone(String phone) { this.phone = phone; }
public void setPlaceName(String placeName) { this.placeName = placeName; }
public void setPlaceUrl(String placeUrl) { this.placeUrl = placeUrl; }
public void setPosX(String posX) { this.posX = posX; }
public void setPosY(String posY) { this.posY = posY; }
public void setRoadAddressName(String roadAddressName) { this.roadAddressName = roadAddressName; } }
public class SameName { @SerializedName("region") String[] region ; @SerializedName("keyword") String keyWord ; @SerializedName("selected_region") String selectedRegion;
public String getSelectedRegion() { return selectedRegion; }
public String getKeyWord() { return keyWord; }
public String[] getRegion() { return region; }
public void setRegion(String[] region) { this.region = region; }
public void setKeyWord(String keyWord) { this.keyWord = keyWord; }
/** * http 호출을 위한 API * 앱 등록 및 사용자 설정 필수 category * * query String 검색을 원하는 질의어 O * category_group_code String 카테고리 그룹 코드 * 결과를 카테고리로 필터링을 원하는 경우 사용 X * x String 중심 좌표의 X값 혹은 longitude * 특정 지역을 중심으로 검색하려고 할 경우 radius와 함께 사용 가능 X * y String 중심 좌표의 Y값 혹은 latitude * 특정 지역을 중심으로 검색하려고 할 경우 radius와 함께 사용 가능 X * radius Integer 중심 좌표부터의 반경거리. 특정 지역을 중심으로 검색하려고 할 경우 중심좌표로 쓰일 x,y와 함께 사용 * 단위 meter, 0~20000 사이의 값 X * rect String 사각형 범위내에서 제한 검색을 위한 좌표. 지도 화면 내 검색시 등 제한 검색에서 사용 가능 * 좌측 X 좌표,좌측 Y 좌표, 우측 X 좌표, 우측 Y 좌표 형식 X * page Integer 결과 페이지 번호 * 1~45 사이의 값 (기본값: 1) X * size Integer 한 페이지에 보여질 문서의 개수 * 1~15 사이의 값 (기본값: 15) X * sort String 결과 정렬 순서, distance 정렬을 원할 때는 기준 좌표로 쓰일 x, y와 함께 사용 * distance 또는 accuracy (기본값: accuracy) X * * category_group_code String 카테고리 코드 O * x String 중심 좌표의 X값 혹은 longitude * 특정 지역을 중심으로 검색하려고 할 경우 radius와 함께 사용 가능. (x,y,radius) 또는 rect 필수 * y String 중심 좌표의 Y값 혹은 latitude * 특정 지역을 중심으로 검색하려고 할 경우 radius와 함께 사용 가능. (x,y,radius) 또는 rect 필수 * radius Integer 중심 좌표부터의 반경거리. 특정 지역을 중심으로 검색하려고 할 경우 중심좌표로 쓰일 x,y와 함께 사용. 단위 meter, 0~20000 사이의 값 (x,y,radius) 또는 rect 필수 * rect String 사각형 범위내에서 제한 검색을 위한 좌표 * 지도 화면 내 검색시 등 제한 검색에서 사용 가능 * 좌측 X 좌표, 좌측 Y 좌표, 우측 X 좌표, 우측 Y 좌표 형식 * x, y, radius 또는 rect 필수 X * page Integer 결과 페이지 번호 * 1~45 사이의 값 (기본값: 1) X * size Integer 한 페이지에 보여질 문서의 개수 * 1~15 사이의 값 (기본값: 15) X * sort String 결과 정렬 순서, distance 정렬을 원할 때는 기준좌표로 쓰일 x, y 파라미터 필요 * distance 또는 accuracy (기본값: accuracy) X */ public interface RetrofitApi { @Headers("Authorization:KakaoAK 647a2bd........0b9d8") @GET("/v2/local/search/keyword.json") Call<ResponseBean> getKeywordData (@Query(value="query", encoded = true) String strAddr, @Query("x") double x, @Query("y") double y, @Query("radius") int radius, @Query("page") int page );
@Headers("Authorization:KakaoAK 647a2........970b9d8") @GET("/v2/local/search/category.json") @GET("/v2/local/search/category.json") Call<ResponseBean> getCategoryData (@Query("category_group_code") String categoryCode, @Query("x") double x, @Query("y") double y, @Query("radius") int radius, @Query("rect") String rect, @Query("page") int page); ); }
여기서 ResponseBean 은 위에서 말한 class 이름이고, @Headers 에 들어 있는 문장에 키값은 위에서 받은 rest api 키 이니 참고 하시길... 자 이제 호출해 볼까 ? 아래와 같이 화면의 버튼 event 등에서 getData을 호출하면 된다.
넘어가는 파라미터는 검색할 때 사용할 strKeyword 그리고 중심이 되는 x, y 좌표(Longitude, Latitude)값, 그리고 반경(radius), 그리고 받아올 페이지의 시작값(iPage)
페이지의 시작값은 난 무조건 1페이지 분량만 받아서 할 거라서 1로 설정해서 받아왔지만, 주변 검색을 더 하고 싶으면 페이지 번호를 계속 변경해서 받아오면 된다. 그 페이지가 마지막 인지 여부를 응답구조체 중에서 meta is_end 이 값을 보고 알 수 있다.
public void getData(String strKeyword, double x, double y, int radius, int iPage) { Log.e(TAG, "(" + x + "," + y + ")" + strKeyword); service.getKeywordData(strKeyword, x, y, radius, iPage).enqueue(new Callback<ResponseBean>() { @Override public void onResponse(Call<ResponseBean> call, Response<ResponseBean> response) { Log.i(TAG, "code=" + response.code() + "" ) ; try { for (ResponseBean.Documents documents : response.body().getDocuments()) { Log.e(TAG, documents.getPlaceName()); MapPoint mapPoint = MapPoint.mapPointWithGeoCoord(documents.getPosY(), documents.getPosX()); addMarker(mapPoint, documents.getPlaceName()); } } catch (Exception e) { e.printStackTrace(); } }
@Override public void onFailure(Call<ResponseBean> call, Throwable t) {
Log.e(TAG, t.toString()) ; t.printStackTrace();
} }); }
그리고 결과를 받아 왔다면 addMarker 함수를 호출해서 지도에 마커들을 출력하면 끝...