'안드로이드'에 해당되는 글 453건

728x90

SearchView 기능을 추가하고 났더니, SearchView 포커스가 자동으로 키 입력을 기다리고 있어서 영 불편하다.

activity_main.xml 파일에서 아래 빨간색으로 표시된 부분을 추가하면 자동으로 포커스가 잡히는 것이 해제된다.


 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <SearchView
        android:id="@+id/search"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:iconifiedByDefault="false"
        android:focusable="false"
        android:focusableInTouchMode="false">

        <requestFocus />
    </SearchView>

    <RelativeLayout
        android:id="@+id/list_view_relative2"
        android:layout_width="match_parent"
        android:layout_height="50dp"
        android:background="@color/whitegray" >

        <CheckBox
            android:id="@+id/lv_checkbox_all"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignParentRight="true"
            android:layout_centerVertical="true"
            android:text="전체선택"
            android:textColor="#000000"
            android:textStyle="bold" />

        <Button
            android:id="@+id/btn_cancle"
            android:layout_width="51dp"
            android:layout_height="33dp"
            android:layout_marginLeft="10dp"
            android:layout_centerVertical="true"
            android:background="@drawable/btn_send_cancel" />

        <Button
            android:id="@+id/btn_send"
            android:layout_width="51dp"
            android:layout_height="33dp"
            android:layout_centerVertical="true"
            android:layout_marginLeft="30dp"
            android:layout_toRightOf="@+id/btn_cancle"
            android:background="@drawable/btn_send_act" />

    </RelativeLayout>

    <ListView
        android:id="@+id/listview"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:layout_below="@+id/search" />

</LinearLayout>


728x90
블로그 이미지

Link2Me

,
728x90

http://link2me.tistory.com/1377 (Android SearchView) 에서 초성검색 기능을 추가했다.

지인에게 보여드렸더니 초성검색 기능도 제공되면 좋겠다고 해서 구글링으로 초성검색을 검색하여 추가해봤다.

http://milkissboy.tistory.com/32 에 나온 코드를 복사해서 Class 를 하나 추가했다.

그리고 http://link2me.tistory.com/1377 에서 수정한 부분만 여기에 기록해둔다.


HangulUtils.java


초성검색되는 코드는 위 참조한 블로그에 있는 것 중에서 main 메소드 부분만 제거하고 나머지는 그대로 이용


수정 코드

 초성검색 없는 코드

        // Filter Class
        public void filter(String charText) {
            charText = charText.toLowerCase(Locale.getDefault());
            lvItemList.clear();
            if (charText.length() == 0) {
                lvItemList.addAll(addressItemList);
            } else {
                for (Address_Item wp : addressItemList) {
                    if (wp.getUserNM().toLowerCase(Locale.getDefault()).contains(charText)) {
                        lvItemList.add(wp);
                    }
                }
            }
            notifyDataSetChanged();
        }

초성검색 추가 코드

        // Filter Class
        public void filter(String charText) {
            charText = charText.toLowerCase(Locale.getDefault());
            lvItemList.clear();
            if (charText.length() == 0) {
                lvItemList.addAll(addressItemList);
            } else {
                for (Address_Item wp : addressItemList) {
                    String iniName = HangulUtils.getHangulInitialSound(wp.getUserNM(), charText);
                    if (iniName.indexOf(charText) >= 0) { // 초성검색어가 있으면 해당 데이터 리스트에 추가
                        lvItemList.add(wp);
                    } else if (wp.getUserNM().toLowerCase(Locale.getDefault()).contains(charText)) {
                        lvItemList.add(wp);
                    }
                }
            }
            notifyDataSetChanged();
        } 


초성검색어 : ㄱㅂ, ㅇㅈ민, 이ㅈㅇ 등 검색어로 이용 가능


728x90
블로그 이미지

Link2Me

,
728x90

Android Studio SearchView 기능을 이용하면 검색처리가 좀 더 편하다.


검색어 자동완성같은 기능을 구현하기 위한 방법이라고 봐도 된다.

먼저, http://abhiandroid.com/ui/searchview 게시글을 다운로드 해서 본인 PC에서 직접 테스트한다.

다운로드 하려면 성명과 E-Mail 을 입력하고 나면 Dropbox 에 올려진 파일을 다운로드 할 수 있다.


아래 내용은 서버에 있는 자료를 ArrayList 에 저장하고 저장된 자료를 기반으로 SearchView 구현에 필요한 것만 발췌한 것이다.


앱 build.gradle 설정 내용 예시




MainActivity.java 주요 내용

변수선언

    ListView listView;
    private ArrayList<Address_Item> addressItemList = new ArrayList<>(); // 서버 원본 데이터 리스트
    private ArrayList<Address_Item> searchItemList = new ArrayList<>(); // 검색한 데이터 리스트
    ListViewAdapter listViewAdapter;
    SearchView editsearch; 

 public void onCreate(Bundle savedInstanceState)

        listView = (ListView) findViewById(R.id.listview);
        listViewAdapter = new ListViewAdapter(this, searchItemList);
        listView.setAdapter(listViewAdapter); // Binds the Adapter to the ListView
        listView.setChoiceMode(ListView.CHOICE_MODE_SINGLE);


        // Adapter에 추가 데이터를 저장하기 위한 ArrayList
        getServerData();

        // Locate the EditText in listview_main.xml
        editsearch = (SearchView) findViewById(R.id.search);
        editsearch.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
            @Override
            public boolean onQueryTextSubmit(String query) {
                // 문자열 입력을 완료했을 때 문자열 반환
                return false;
            }

            @Override
            public boolean onQueryTextChange(String newText) {
                // 문자열이 변할 때마다 바로바로 문자열 반환
                String text = newText;
                listViewAdapter.filter(text);
                return false;
            }
        });

     private void getServerData() {
        settings = getSharedPreferences("settings", Activity.MODE_PRIVATE);
        Uri.Builder builder = new Uri.Builder()
                .appendQueryParameter("idx", settings.getString("idx",""));
        String postParams = builder.build().getEncodedQuery();
        new getJSONData().execute(Value.IPADDRESS + "/get_json.php",postParams);
    }


    class getJSONData extends AsyncTask<String, Void, String> {
        @Override
        protected void onPreExecute() {
            super.onPreExecute();
            // Create a progressdialog
            mProgressDialog = new ProgressDialog(MainActivity.this);
            mProgressDialog.setTitle("Personal Profile JSON Parse");
            mProgressDialog.setMessage("Loading...");
            mProgressDialog.setIndeterminate(false);
            mProgressDialog.show();  // Show progressdialog
        }

        @Override
        protected String doInBackground(String... params) {
            try {
                return PHPComm.getJson(params[0],params[1]);
            } catch (Exception e) {
                return new String("Exception: " + e.getMessage());
            }
        }

        protected void onPostExecute(String result){
            searchJSON=result;
            showList();
            mProgressDialog.dismiss();
        }
    }


    // 아이템 데이터 추가를 위한 메소드
    public void getServerDataList(String photo_image, String uid, String name, String mobileNO, String officeNO, boolean checkItem_flag) {
        Address_Item item = new Address_Item();
        item.setPhoto(photo_image);
        item.setIdx(uid);
        item.setUserNM(name);
        item.setMobileNO(mobileNO);
        item.setOfficeNO(officeNO);
        item.setCheckBoxState(checkItem_flag);

        addressItemList.add(item);
    }

    // 선택한 데이터 추가를 위한 메소드
    public void selectDataList(String photo_image, String uid, String name, String mobileNO, String officeNO, boolean checkItem_flag) {
        Address_Item item = new Address_Item();
        item.setPhoto(photo_image);
        item.setIdx(uid);
        item.setUserNM(name);
        item.setMobileNO(mobileNO);
        item.setOfficeNO(officeNO);
        item.setCheckBoxState(checkItem_flag);

        searchItemList.add(item);
    }

     // 서버 정보를 파싱하기 위한 변수 선언
    String searchJSON;
    private static final String TAG_RESULTS="result";
    private static final String TAG_UID = "uid"; // 서버 테이블의 실제 필드명
    private static final String TAG_NAME = "userNM";
    private static final String TAG_MobileNO ="mobileNO";
    private static final String TAG_OfficeNO ="telNO";
    private static final String TAG_Image = "photo"; // 이미지 필드
    JSONArray peoples = null;

    protected void showList() {
        try {
            JSONObject jsonObj = new JSONObject(searchJSON);
            peoples = jsonObj.getJSONArray(TAG_RESULTS);

            addressItemList.clear(); // 서버에서 가져온 데이터 초기화
            for(int i=0;i < peoples.length();i++){
                JSONObject c = peoples.getJSONObject(i);
                final String uid = c.getString(TAG_UID);
                final String name = c.getString(TAG_NAME);
                final String mobileNO = c.getString(TAG_MobileNO);
                final String officeNO = c.getString(TAG_OfficeNO);
                final String Photo_Image = c.getString(TAG_Image);

                // 서버에서 가져온 데이터 저장 (원본 보관용과 검색용 사용 목적)
                getServerDataList(Photo_Image, uid, name, mobileNO, officeNO,false);
                selectDataList(Photo_Image, uid, name, mobileNO, officeNO,false);
            }

            runOnUiThread(new Runnable() { // 화면에 반영하기 위하여 실시간 갱신한다.
                @Override
                public void run() {
                    // 갱신된 데이터 내역을 어댑터에 알려줌
                    listViewAdapter.notifyDataSetChanged();
                }
            });
        } catch (JSONException e) {
            e.printStackTrace();
        }

    }

 ListViewAdapter 주요 부분
    private class ListViewAdapter extends BaseAdapter {
        ImageLoader imageLoader;
        Context context;
        LayoutInflater inflater;
        private List<Address_Item> lvItemList = null;

        public ListViewAdapter(Context context, List<Address_Item> items) {
            this.context = context;
            imageLoader = new ImageLoader(context);
            lvItemList = items;
        }

        public class ViewHolder {
            LinearLayout child_layout;
            ImageView photo_Image;
            TextView tv_name;
            TextView tv_mobileNO;
            TextView tv_officeNO;
            ImageView child_btn;
            CheckBox checkbox;
        }

        @Override
        public int getCount() {
            return lvItemList.size();
        }

        @Override
        public Object getItem(int position) {
            return lvItemList.get(position);
        }

        // 지정한 위치(position)에 있는 데이터 리턴
        @Override
        public long getItemId(int position) {
            return position;
        }

        public View getView(final int position, View convertView, ViewGroup parent) {
            final ViewHolder viewHolder;
            final Context context = parent.getContext();
            final Integer index = Integer.valueOf(position);

            // 화면에 표시될 View
            if(convertView == null){
                viewHolder = new ViewHolder();

                LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
                convertView = inflater.inflate(R.layout.address_item,parent,false);

                convertView.setBackgroundColor(0x00FFFFFF);
                convertView.invalidate();

                // 화면에 표시될 View 로부터 위젯에 대한 참조 획득
                viewHolder.photo_Image = (ImageView) convertView.findViewById(R.id.profile_Image);
                viewHolder.tv_name = (TextView) convertView.findViewById(R.id.child_name);
                viewHolder.tv_mobileNO = (TextView) convertView.findViewById(R.id.child_mobileNO);
                viewHolder.tv_officeNO = (TextView) convertView.findViewById(R.id.child_officeNO);
                viewHolder.child_btn = (ImageView) convertView.findViewById(R.id.child_Btn);
                viewHolder.checkbox = (CheckBox) convertView.findViewById(R.id.list_cell_checkbox);

                convertView.setTag(viewHolder);
            } else {
                viewHolder = (ViewHolder) convertView.getTag();
            }

            // PersonData 에서 position 에 위치한 데이터 참조 획득
            final Address_Item addressItem = lvItemList.get(position);

            if(addressItem.getPhoto().equals("")){
                final Bitmap Base_Profile = PHPComm.autoresize_decodeResource(getResources(), R.drawable.photo_base, 160);
                viewHolder.photo_Image.setImageBitmap(Base_Profile);
            } else {
                String photoURL = Value.IPADDRESS + "/photos/" + addressItem.getPhoto();
                imageLoader.DisplayImage(photoURL, viewHolder.photo_Image);
            }

            viewHolder.tv_name.setText(addressItem.getUserNM());
            viewHolder.tv_mobileNO.setText(PhoneNumberUtils.formatNumber(addressItem.getMobileNO()));
            viewHolder.tv_officeNO.setText(PhoneNumberUtils.formatNumber(addressItem.getOfficeNO()));

            convertView.setOnLongClickListener(new View.OnLongClickListener() {
                @Override
                public boolean onLongClick(View view) {
                    isMSG = true;
                    relative2.setVisibility(View.VISIBLE);
                    listViewAdapter.notifyDataSetChanged();
                    return false;
                }
            });

            if (isMSG == false) {
                viewHolder.child_btn.setVisibility(View.VISIBLE);
                viewHolder.child_btn.setOnClickListener(new View.OnClickListener() {
                    @Override
                    public void onClick(View view) {
                        builder.show();
                    }
                });
                viewHolder.checkbox.setVisibility(View.GONE);
                convertView.setOnClickListener(new View.OnClickListener() {
                    @Override
                    public void onClick(View view) {
                        Toast.makeText(getApplicationContext(), "상세보기를 눌렀습니다 ===" + addressItem.getIdx(), Toast.LENGTH_SHORT).show();
                    }
                });
            } else {
                if (isMSG == true) {
                    viewHolder.child_btn.setVisibility(View.GONE);
                    viewHolder.checkbox.setVisibility(View.VISIBLE);
                    viewHolder.checkbox.setTag(position); // This line is important.

                    viewHolder.checkbox.setOnClickListener(new View.OnClickListener() {
                        @Override
                        public void onClick(View v) {
                            if(lvItemList.get(position).isCheckBoxState() == true){
                                lvItemList.get(position).setCheckBoxState(false);
                            } else {
                                lvItemList.get(position).setCheckBoxState(true);
                            }
                        }
                    });

                    convertView.setOnClickListener(new View.OnClickListener() {
                        @Override
                        public void onClick(View v) {
                            if(viewHolder.checkbox.isChecked() == false){
                                viewHolder.checkbox.setChecked(true);
                                lvItemList.get(position).setCheckBoxState(true);
                            } else {
                                viewHolder.checkbox.setChecked(false);
                                lvItemList.get(position).setCheckBoxState(false);
                            }
                        }
                    });

                }
            }

            // 재사용 문제 해결
            if(lvItemList.get(position).isCheckBoxState() == true){
                viewHolder.checkbox.setChecked(true);
            } else {
                viewHolder.checkbox.setChecked(false);
            }

            return convertView;
        }

        // Filter Class
        public void filter(String charText) {
            charText = charText.toLowerCase(Locale.getDefault());
            lvItemList.clear();
            if (charText.length() == 0) {
                lvItemList.addAll(addressItemList);
            } else {
                for (Address_Item wp : addressItemList) {
                    if (wp.getUserNM().toLowerCase(Locale.getDefault()).contains(charText)) {
                        lvItemList.add(wp);
                    }
                }
            }
            notifyDataSetChanged();
        }

    }


foreach 문의 기본적인 내용은 http://link2me.tistory.com/1204 참조하면 된다.

728x90
블로그 이미지

Link2Me

,
728x90

Android Studio 샘플 예제를 받은 걸 가지고 패키지명 변경을 하는 방법이다.


먼저 폴더를 선택한 다음에 3번까지 하고 나면, 폴더가 펼쳐져서 보인다.



이제 변경하고 싶은 곳에 마우스로 선택한 다음, Shift + F6 키를 누르면 패키지명 변경할 수 있게 나온다.



패키지명을 입력하고 Refactor 를 해주면 Do Refactor 가 나온다. 눌러주면 변경된다.


다른 폴더명도 이렇게 변경해주면 된다.


만약 폴더가 모자라면 수동으로 직접 폴더명을 생성해주면 자동으로 인식된다.

그러고나서, 파일을 마우스로 Drag & Drop 으로 해당 폴더에 옮겨준다.


이렇게 하고 나면 AndroidManifest.xml 파일이 변경된 것을 확인할 수 있다.


그런데 자동으로 변경되지 않는 것이 있다.

앱 build.gradle 정보는 변경되지 않는다.

applicationId 값을 수작업으로 변경해줘야 한다.

이걸 안해주었더니 "Error: Activity class does not exist." 가 나와서 에러 메시지 찾아서 수정하느라고 생쑈를 조금 했다.


apply plugin: 'com.android.application'

android {
    compileSdkVersion 23
    buildToolsVersion "25.0.0"

    defaultConfig {
        applicationId "com.tistory.link2me.searchview"
        minSdkVersion 15
        targetSdkVersion 23
        versionCode 1
        versionName "1.0"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile 'com.android.support:appcompat-v7:23.1.1'
} 



728x90
블로그 이미지

Link2Me

,
728x90

정보은닉(Information Hiding)
- 객체가 실행되는 과정이나 속성을 외부로부터 감추는 것.
  즉, 인스턴스 변수에 바로 접근 하지 못하도록 Private 선언을 해주는 것이다.
- private : Class 내부에서만 접근 가능
- public : 어디서든 접근 가능
- default : 접근제어 지시자를 선언하지 않은 경우로 동일 패키지 내에서는 접근 가능
- protected : 상속관계에 놓여 있어도 접근을 허용


캡슐화(encapsulation)
- 캡슐화는 메소드와 변수들을 클래스 하나로 묶어버리는 것
- 객체 외부에서는 개체 내부 정보를 직접 접근하거나 조작할 수 없고, 외부에서 접근할 수 있도록 정의된 오퍼레이션을 통해서만 관련 데이터에 접근할 수 있다.(getter, setter)
- 객체들 간의 메시지를 주고 받을 때 각 객체의 세부 내용은 알 필요가 없으므로 인터페이스가 간결해지고, 객체간의 결합도가 낮아진다.



Eclipse 에서 getter, setter 를 자동으로 생성하는 기능을 제공하고 있다.

public class Person1 {
    // 자바의 기본 원칙은 클래스 1개당 1개의 파일
    public String name; // 필드, 데이터 멤버
    public String number; //
} 

public 을 private 로 둘다 변경하고 나서,

Alt + Shlift + S + R를 누르면 팝업된 창이 나온다.

Alt + A 를 누르거나 마우스로 둘다 선택한 다음에

Generate 를 해주면 자동으로 생성된다.

public class Person1 {
    private String name; // 필드, 데이터 멤버
    private String number; 
   
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public String getNumber() {
        return number;
    }
    public void setNumber(String number) {
        this.number = number;
    }
}


728x90

'안드로이드 > Java 문법' 카테고리의 다른 글

[Java] instanceof 연산자  (0) 2017.10.24
Java static 변수 이해하기  (0) 2017.10.20
자바 기본 데이터형과 크기  (0) 2017.10.11
Java 클래스(Class) ★★★★★  (0) 2017.05.04
ArrayList and HashMap  (0) 2017.03.06
블로그 이미지

Link2Me

,
728x90

RecyclerView는 롤리팝 버전에서 새로이 등장한 위젯으로 안드로이드 ListView의 장/단점을 보완했다.

RecyclerView

ListView

 수직 스크롤, 수평 스크롤 지원

 수직 스크롤 지원

 ViewHolder 패턴 강제 적용

 (재활용 View 를 관리해주는 Class)

 ViewHolder 패턴 선택 적용

 Animation 적용이 쉽다
 (ItemAnimation : Item 항목의 Animation 관리 Class)

 Animation 적용이 어렵다


Target API에 따라서 숫자가 변경되니 Google SupportLibrary 사이트에서 확인해야 한다.
지원 라이브러리에 대한 내용은 https://developer.android.com/topic/libraries/support-library/features.html#v7 를 참조하면 도움된다.


Audio Player 에서 음악파일을 ListView 에 보여주는 걸 RecyclerView 로 변경하는 걸 해보고 신경써서 살펴볼 것만  적어둔다.

- 체크박스 선택 기능 및 전체 선택/해제 기능

- 버튼을 길게 누르면 체크박스를 선택할 수 있는 기능 (다중 선택)


아래 기술된 사항은 체크박스가 들어간 RecylcerView 라고 보면 된다.

보통 ListView 예제를 찾아보면 체크박스 선택하는 기능이 추가된 것은 찾기가 어렵다. 체크박스 선택 기능이 들어가면 코드 구현시 주의해야 할 사항이 있다.

아래 코드에는 ListView, RecyclerView 모두 체크박스 처리 부분이 들어가 있는데, RecyclerView 를 완벽하게 활용하지 못하므로 코드가 매끄럽지 못할 수 있다.


가장 먼저 앱 build.gradle 을 다음과 같이 추가해줬다. 이건 어디까지나 참고사항이므로 꼭 이대로 할 필요는 없다.

compileSdkVersion 23 으로 변경해서 테스트를 했다. 버전이 너무 높다고 좋은게 아닌거 같아서다.


apply plugin: 'com.android.application'

android {
    compileSdkVersion 23
    buildToolsVersion "25.0.0"

    defaultConfig {
        applicationId "com.tistory.link2me.recyclerview"
        minSdkVersion 19
        targetSdkVersion 23
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"

    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile 'com.android.support:appcompat-v7:23.4.0'
    compile 'com.android.support.constraint:constraint-layout:1.0.2'
    compile 'com.squareup.okhttp3:okhttp:3.9.0' // 서버와의 통신 라이브러리
    compile 'com.squareup.picasso:picasso:2.5.2' // 이미지 라이브러리
    compile 'com.android.support:recyclerview-v7:24.2.0' // ListView 개선 버전
    compile "com.android.support:cardview-v7:24.2.0"
    compile "com.android.support:design:24.2.0" // snack bar
}


dependencies 부분은 필요한 부분만 추가하면 된다.

RecyclerView 부분은 compile 'com.android.support:recyclerview-v7:24.2.0' 만 추가하면 된다.



activity_main.xml 파일에서 변경된 부분

ListView 부분이 RecyclerView 위젯으로 변경하면 된다. (색깔로 표시한 부분)

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" >

    <RelativeLayout
        android:id="@+id/list_view_relative2"
        android:layout_width="match_parent"
        android:layout_height="50dp"
        android:background="@color/whitegray" >

        <CheckBox
            android:id="@+id/lv_checkbox_all"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignParentRight="true"
            android:layout_centerVertical="true"
            android:text="전체선택"
            android:textColor="#000000"
            android:textStyle="bold" />

        <Button
            android:id="@+id/btn_cancle"
            android:layout_width="51dp"
            android:layout_height="33dp"
            android:layout_marginLeft="10dp"
            android:layout_centerVertical="true"
            android:background="@drawable/btn_send_cancel" />

        <Button
            android:id="@+id/btn_send"
            android:layout_width="51dp"
            android:layout_height="33dp"
            android:layout_centerVertical="true"
            android:layout_marginLeft="30dp"
            android:layout_toRightOf="@+id/btn_cancle"
            android:background="@drawable/btn_send_act" />

    </RelativeLayout>

    <ListView
        android:id="@+id/my_listView"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />
</LinearLayout>

 <?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <RelativeLayout
        android:id="@+id/list_view_relative2"
        android:layout_width="match_parent"
        android:layout_height="50dp"
        android:background="@color/whitegray" >

        <CheckBox
            android:id="@+id/lv_checkbox_all"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignParentRight="true"
            android:layout_centerVertical="true"
            android:text="전체선택"
            android:textColor="#000000"
            android:textStyle="bold" />

        <Button
            android:id="@+id/btn_cancle"
            android:layout_width="51dp"
            android:layout_height="33dp"
            android:layout_marginLeft="10dp"
            android:layout_centerVertical="true"
            android:background="@drawable/btn_send_cancel" />

        <Button
            android:id="@+id/btn_send"
            android:layout_width="51dp"
            android:layout_height="33dp"
            android:layout_centerVertical="true"
            android:layout_marginLeft="30dp"
            android:layout_toRightOf="@+id/btn_cancle"
            android:background="@drawable/btn_send_act" />

    </RelativeLayout>

    <android.support.v7.widget.RecyclerView
        android:id="@+id/recyclerview"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:scrollbars="vertical"/>

</LinearLayout>


MainActivity.java 파일

변수 선언 부분

   private ListView audiolistView; // 리스트뷰
    private ArrayList<Song_Item> songsList = null; // 음악 전체 데이터 리스트
    private ArrayList<Song_Item> playList = new ArrayList<>(); // 선택한 음악 데이터 리스트
    private ListViewAdapter listViewAdapter = null; // 리스트뷰에 사용되는 ListViewAdapter

    RecyclerView audiolistView; // 리스트뷰 (ListView 대신 RecyclerView)
    private ArrayList<Song_Item> songsList = new ArrayList<>(); // 음악 전체 데이터 리스트
    private ArrayList<Song_Item> playList = new ArrayList<>(); // 선택한 음악 데이터 리스트
    RecyclerView.Adapter listViewAdapter; // ListViewAdapter 대신 RecyclerView.Adapter
    RecyclerView.LayoutManager layoutManager;


protected void onCreate(Bundle savedInstanceState) 부분

       // Adapter에 추가 데이터를 저장하기 위한 ArrayList
        songsList = new ArrayList<Song_Item>(); // ArrayList 생성
        audiolistView = (ListView) findViewById(R.id.my_listView);
        listViewAdapter = new ListViewAdapter(this); // Adapter 생성
        audiolistView.setAdapter(listViewAdapter); // 어댑터를 리스트뷰에 세팅
        audiolistView.setChoiceMode(ListView.CHOICE_MODE_SINGLE);

         // Adapter에 추가 데이터를 저장하기 위한 ArrayList
        audiolistView = (RecyclerView) findViewById(R.id.recyclerview);
        audiolistView.setHasFixedSize(true);

        LinearLayoutManager layoutManager = new LinearLayoutManager(this);
        layoutManager.setOrientation(LinearLayoutManager.VERTICAL);
        audiolistView.setLayoutManager(layoutManager);

        listViewAdapter = new ListViewAdapter(songsList,this); // Adapter 생성
        audiolistView.setAdapter(listViewAdapter); // 어댑터를 리스트뷰에 세팅


체크박스 전체 선택/취소 버튼 부분

ListView 에서는 ListViewAdapter 부분에 전체 선택/해제 할 수 있는 기능을 넣어도 문제가 없었는데,

에러가 발생해서 public void setAllChecked(final boolean ischeked) 메소드 위치를 변경해서 해결했다.

        // all checkbox
        checkAll = (CheckBox) findViewById(R.id.lv_checkbox_all);
        checkAll.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
            @Override
            public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
                if (checkAll.isChecked() == true) {
                    isAll = true;
                    listViewAdapter.setAllChecked(checkAll.isChecked());
                    listViewAdapter.notifyDataSetChanged();
                } else {
                    isAll = false;
                    listViewAdapter.setAllChecked(checkAll.isChecked());
                    listViewAdapter.notifyDataSetChanged();
                }
            }
        });

        final Button calcel = (Button) findViewById(R.id.btn_cancle);
        calcel.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                isMSG = false;
                relative2.setVisibility(View.GONE);
                listViewAdapter.setAllChecked(false);
                checkAll.setChecked(false); // 전체 선택 체크박스 해제
                listViewAdapter.notifyDataSetChanged();
            }
        });

         // all checkbox
        checkAll = (CheckBox) findViewById(R.id.lv_checkbox_all);
        checkAll.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
            @Override
            public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
                if (checkAll.isChecked() == true) {
                    isAll = true;
                    setAllChecked(checkAll.isChecked());
                    listViewAdapter.notifyDataSetChanged();
                } else {
                    isAll = false;
                    setAllChecked(checkAll.isChecked());
                    listViewAdapter.notifyDataSetChanged();
                }
            }
        });

        final Button calcel = (Button) findViewById(R.id.btn_cancle);
        calcel.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                isMSG = false;
                relative2.setVisibility(View.GONE);
                setAllChecked(false);
                checkAll.setChecked(false); // 전체 선택 체크박스 해제
                listViewAdapter.notifyDataSetChanged();
            }
        });


음악데이터 추가 메소드도 위치를 변경해서 해결했다.


ListView 기반 ViewHolder 와 ListViewAdapter 부분

    class ViewHolder {
        public ImageView mImgAlbumArt;
        public TextView mTitle;
        public TextView mSubTitle;
        public TextView mDuration;
        public CheckBox checkbox;
    }

    private class ListViewAdapter extends BaseAdapter {
        Context context;
        public final Uri artworkUri = Uri.parse("content://media/external/audio/albumart");

        public ListViewAdapter(Context context) {
            this.context = context;
        }

        // 음악 데이터 추가를 위한 메소드
        public void addItem(long mId, long AlbumId, String Title, String Artist, String Album, Integer Duration, String DataPath, boolean checkItem_flag) {
            Song_Item item = new Song_Item();
            item.setmId(mId);
            item.setAlbumId(AlbumId);
            item.setTitle(Title);
            item.setArtist(Artist);
            item.setAlbum(Album);
            item.setDuration(Duration);
            item.setDataPath(DataPath);
            item.setCheckBoxState(checkItem_flag);
            songsList.add(item);
        }

        // CheckBox를 모두 선택하는 메서드
        public void setAllChecked(final boolean ischeked) {
            final int tempSize = songsList.size();
            if (isAll == true) {
                for (int i = 0; i < tempSize; i++) {
                    songsList.get(i).setCheckBoxState(true);
                }
            } else {
                for (int i = 0; i < tempSize; i++) {
                    songsList.get(i).setCheckBoxState(false);
                }
            }
        }

        @Override
        public int getCount() {
            return songsList.size(); // 데이터 개수 리턴
        }

        @Override
        public Object getItem(int position) {
            return songsList.get(position);
        }

        // 지정한 위치(position)에 있는 데이터 리턴
        @Override
        public long getItemId(int position) {
            return position;
        }

        // position에 위치한 데이터를 화면에 출력하는데 사용될 View를 리턴
        @Override
        public View getView(final int position, View convertView, ViewGroup parent) {
            final ViewHolder viewHolder;
            final Context context = parent.getContext();
            final Integer index = Integer.valueOf(position);

            // 화면에 표시될 View
            if (convertView == null) {
                viewHolder = new ViewHolder();

                LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
                convertView = inflater.inflate(R.layout.song_item, parent, false);

                convertView.setBackgroundColor(0x00FFFFFF);
                convertView.invalidate();

                // 화면에 표시될 View 로부터 위젯에 대한 참조 획득
                viewHolder.mImgAlbumArt = (ImageView) convertView.findViewById(R.id.album_Image);
                viewHolder.mTitle = (TextView) convertView.findViewById(R.id.txt_title);
                viewHolder.mSubTitle = (TextView) convertView.findViewById(R.id.txt_subtitle);
                viewHolder.mDuration = (TextView) convertView.findViewById(R.id.txt_duration);
                viewHolder.checkbox = (CheckBox) convertView.findViewById(R.id.list_cell_checkbox);

                convertView.setTag(viewHolder);
            } else {
                viewHolder = (ViewHolder) convertView.getTag();
            }

            // Song_Item 에서 position 에 위치한 데이터 참조 획득
            final Song_Item songItem = songsList.get(position);

            // 아이템 내 각 위젯에 데이터 반영
            Uri albumArtUri = ContentUris.withAppendedId(artworkUri, songItem.getAlbumId());
            Picasso.with(convertView.getContext()).load(albumArtUri).error(R.drawable.ic_launcher).into(viewHolder.mImgAlbumArt);
            viewHolder.mTitle.setText(songItem.getTitle());
            viewHolder.mSubTitle.setText(songItem.getArtist());

            int dur = (int) songItem.getDuration();
            int hrs = (dur / 3600000);
            int mns = (dur / 60000) % 60000;
            int scs = dur % 60000 / 1000;
            String songTime = String.format("%02d:%02d:%02d", hrs, mns, scs);
            if (hrs == 0) {
                songTime = String.format("%02d:%02d", mns, scs);
            }
            viewHolder.mDuration.setText(songTime);

            if (isMSG == false) {
                viewHolder.checkbox.setVisibility(View.GONE);
                convertView.setOnClickListener(new View.OnClickListener() {
                    @Override
                    public void onClick(View view) {
                        playList.clear();
                        PlayList(songsList.get(position).getmId(), songsList.get(position).getAlbumId(), songsList.get(position).getTitle(), songsList.get(position).getArtist(),
                                songsList.get(position).getAlbum(), songsList.get(position).getDuration(), songsList.get(position).getDataPath());

                        System.out.println("position=" + position + " 노래제목 : " + songsList.get(position).getTitle());

                        Intent intent = new Intent(MainActivity.this, Player.class);
                        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | intent.FLAG_ACTIVITY_CLEAR_TOP);
                        intent.putExtra("playList", playList); // 배열 데이터
                        startActivity(intent);
                    }
                });

                convertView.setOnLongClickListener(new View.OnLongClickListener() {
                    @Override
                    public boolean onLongClick(View view) {
                        isMSG = true;
                        relative2.setVisibility(View.VISIBLE);
                        listViewAdapter.notifyDataSetChanged();
                        return false;
                    }
                });

            } else {
                if (isMSG == true) {
                    viewHolder.checkbox.setVisibility(View.VISIBLE);
                    viewHolder.checkbox.setTag(position); // This line is important.

                    viewHolder.checkbox.setOnClickListener(new View.OnClickListener() {
                        @Override
                        public void onClick(View v) {
                            if (songsList.get(position).isCheckBoxState() == true) {
                                songsList.get(position).setCheckBoxState(false);
                            } else {
                                songsList.get(position).setCheckBoxState(true);
                            }
                        }
                    });

                    convertView.setOnClickListener(new View.OnClickListener() {
                        @Override
                        public void onClick(View v) {
                            if (viewHolder.checkbox.isChecked() == false) {
                                viewHolder.checkbox.setChecked(true);
                                songsList.get(position).setCheckBoxState(true);
                            } else {
                                viewHolder.checkbox.setChecked(false);
                                songsList.get(position).setCheckBoxState(false);
                            }
                        }
                    });

                }
            }

            // 재사용 문제 해결
            if (songsList.get(position).isCheckBoxState() == true) {
                viewHolder.checkbox.setChecked(true);
            } else {
                viewHolder.checkbox.setChecked(false);
            }

            return convertView;
        }

    }


RecyclerView 기반 ListViewAdapter 부분

    class ListViewAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
        Context mContext;
        private ArrayList<Song_Item> songsList;
        Song_Item songItem;
        int Position;

        public final Uri artworkUri = Uri.parse("content://media/external/audio/albumart");

        public ListViewAdapter(ArrayList<Song_Item> items,Context context) {
            songsList = items;
            mContext = context;
        }

        public class ViewHolder extends RecyclerView.ViewHolder  {
            public ImageView mImgAlbumArt;
            public TextView mTitle;
            public TextView mSubTitle;
            public TextView mDuration;
            public CheckBox checkbox;

            public ViewHolder(final View itemView) {
                super(itemView);
                // 화면에 표시될 View 로부터 위젯에 대한 참조 획득
                mImgAlbumArt = (ImageView) itemView.findViewById(R.id.album_Image);
                mTitle = (TextView) itemView.findViewById(R.id.txt_title);
                mSubTitle = (TextView) itemView.findViewById(R.id.txt_subtitle);
                mDuration = (TextView) itemView.findViewById(R.id.txt_duration);
                checkbox = (CheckBox) itemView.findViewById(R.id.list_cell_checkbox);

                itemView.setOnLongClickListener(new View.OnLongClickListener() {
                    @Override
                    public boolean onLongClick(View view) {
                        isMSG = true;
                        relative2.setVisibility(View.VISIBLE);
                        listViewAdapter.notifyDataSetChanged();
                        return false;
                    }
                });

                itemView.setOnClickListener(new View.OnClickListener() {
                    @Override
                    public void onClick(View view) {
                        Position = getAdapterPosition();
                        if (isMSG == false) {
                            playList.clear();
                            PlayList(songsList.get(Position).getmId(), songsList.get(Position).getAlbumId(), songsList.get(Position).getTitle(), songsList.get(Position).getArtist(),
                                    songsList.get(Position).getAlbum(), songsList.get(Position).getDuration(), songsList.get(Position).getDataPath());

                            System.out.println("position=" + Position + " 노래제목 : " + songsList.get(Position).getTitle());

                            Intent intent = new Intent(MainActivity.this, Player.class);
                            intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | intent.FLAG_ACTIVITY_CLEAR_TOP);
                            intent.putExtra("playList", playList); // 배열 데이터
                            mContext.startActivity(intent);
                        } else {
                            //Toast.makeText(mContext, ""+Position, Toast.LENGTH_SHORT).show();
                            if (checkbox.isChecked() == false) {
                                checkbox.setChecked(true);
                                songsList.get(Position).setCheckBoxState(true);
                            } else {
                                checkbox.setChecked(false);
                                songsList.get(Position).setCheckBoxState(false);
                            }
                        }

                    }
                });

            }

            public void setAudioItem(Song_Item item, final int position) {
                songItem = item;
                // 아이템 내 각 위젯에 데이터 반영
                Uri albumArtUri = ContentUris.withAppendedId(artworkUri, songItem.getAlbumId());
                Picasso.with(mContext).load(albumArtUri).error(R.drawable.ic_launcher).into(mImgAlbumArt);
                mTitle.setText(songItem.getTitle());
                mSubTitle.setText(songItem.getArtist());

                int dur = (int) songItem.getDuration();
                int hrs = (dur / 3600000);
                int mns = (dur / 60000) % 60000;
                int scs = dur % 60000 / 1000;
                String songTime = String.format("%02d:%02d:%02d", hrs, mns, scs);
                if (hrs == 0) {
                    songTime = String.format("%02d:%02d", mns, scs);
                }
                mDuration.setText(songTime);

                if (isMSG == false) {
                    checkbox.setVisibility(View.GONE);
                } else {
                    if (isMSG == true) {
                        checkbox.setVisibility(View.VISIBLE);
                        checkbox.setTag(position); // This line is important.
                        checkbox.setOnClickListener(new View.OnClickListener() {
                            @Override
                            public void onClick(View v) {
                                //Toast.makeText(mContext, ""+position, Toast.LENGTH_SHORT).show();
                                if (songsList.get(position).isCheckBoxState() == true) {
                                    songsList.get(position).setCheckBoxState(false);
                                } else {
                                    songsList.get(position).setCheckBoxState(true);
                                }
                            }
                        });
                    }
                }

                // 재사용 문제 해결
                if (songsList.get(position).isCheckBoxState() == true) {
                    checkbox.setChecked(true);
                } else {
                    checkbox.setChecked(false);
                }
            }

        }

        @Override
        public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
            // 새로운 뷰를 만든다.
            View itemView = LayoutInflater.from(parent.getContext()).inflate(R.layout.song_item,parent,false);
            ViewHolder viewHolder = new ViewHolder(itemView);
            return viewHolder;
        }

        @Override
        public void onBindViewHolder(final RecyclerView.ViewHolder holder, int position) {
            // ListView의 getView 부분을 담당하는 메소드
            // Song_Item 에서 position 에 위치한 데이터 참조 획득
            songItem = songsList.get(position);
            ((ViewHolder) holder).setAudioItem(songItem,position);
        }

        @Override
        public int getItemCount() {
            return songsList.size(); // 데이터 개수 리턴
        }

    }


이런 부분을 수정해서 RecyclerView 기반으로 변경하는데 성공했다.

좀 더 다른 예제도 활용해서 테스트를 해보고 RecyclerView 의 장점(?)을 파악해보련다.

728x90
블로그 이미지

Link2Me

,
728x90

자바 기본 데이터형 크기에 대한 사항이다.

데이터형
예약어
비트수
범위
①논리형
boolean
8bit
(1바이트)
true, false
②문자형
char

16bit
유니코드
(2바이트)

수치로는 0 ~ 65535('\u0000'~'\uFFFF')

유니코드:영어,숫자-1바이트, 그외 다국어-2바이트

③수치형
(정수)
byte
8bit
(1바이트)
-128 ~ 127(-2의7제곱~2의7제곱-1)
개수:2의8제곱
④수치형
(정수)
short
16bit
(2바이트)
-32,768 ~ 32,767(-2의15제곱~2의15제곱-1)
개수:2의16제곱
⑤수치형
(정수)
int
32bit
(4바이트)
-2,147,483,648 ~ 2,147,483,647(-2의31제곱~2의31제곱-1)
개수:2의32제곱
⑥수치형
(정수)
long
64bit
(8바이트)
-9,223,372,036,854,775,808~9,223,372,036,854,775,807
(-2의63제곱~2의63제곱-1)
개수:2의64제곱
⑦수치형
(실수형)
float
32bit
(4바이트)
±3.40282347E+38, ±1.40239846E-45,
IEEE 754-1985표준
⑧수치형
(실수형)
double
64bit
(8바이트)
-1.79769313486231570E308~+1.79769313486231570E308

 



출처: http://aventure.tistory.com/59 [H.A.P.P.Y]

 

데이터형
예약어
비트수
범위
①논리형
boolean
8bit
(1바이트)
true, false
②문자형
char

16bit
유니코드
(2바이트)

수치로는 0 ~ 65535('\u0000'~'\uFFFF')

유니코드:영어,숫자-1바이트, 그외 다국어-2바이트

③수치형
(정수)
byte
8bit
(1바이트)
-128 ~ 127(-2의7제곱~2의7제곱-1)
개수:2의8제곱
④수치형
(정수)
short
16bit
(2바이트)
-32,768 ~ 32,767(-2의15제곱~2의15제곱-1)
개수:2의16제곱
⑤수치형
(정수)
int
32bit
(4바이트)
-2,147,483,648 ~ 2,147,483,647(-2의31제곱~2의31제곱-1)
개수:2의32제곱
⑥수치형
(정수)
long
64bit
(8바이트)
-9,223,372,036,854,775,808~9,223,372,036,854,775,807
(-2의63제곱~2의63제곱-1)
개수:2의64제곱
⑦수치형
(실수형)
float
32bit
(4바이트)
±3.40282347E+38, ±1.40239846E-45,
IEEE 754-1985표준
⑧수치형
(실수형)
double
64bit
(8바이트)
-1.79769313486231570E308~+1.79769313486231570E308

출처: http://aventure.tistory.com/59 [H.A.P.P.Y]

위 표에 대한 출처는 위 사이트고

아래 내용을 "난 Java를 공부한 적이 없다구요" 동영상 강좌 자료를 들으면서 기본 개념 잡는데 많은 도움이 될 거 같아서 안드로이드 개발 도움 관점으로 요약 정리하고 있다.

자바의 정석 책을 부분 부분 참조를 하다보니 이해를 못하고 넘어가는 경우가 많다. 결국 개발시간을 허비하는 결과로 이어지기 일쑤여서 기본기를 다지는 것이 중요하다는 느끼는데 막상 자바 혼자 공부하려면 쉽지 않더라.


자동형변환 규칙(Impict conversion)


명시적 형변환 :  자동형변환 발생시점 표시를 위해서, 규칙에 위배되지만 변환이 필요한 상황

- int num2 = (int) num1;

- logn num4 = (long) num3;

- 형 변환 나눗셈 예제 : (float)9/4; // 먼저 9가 형변환되어 9.0f 로 변경되고 4 가 4.0f로 변환되어 연산 수행

  . 피 연산자가 정수면 정수형 연산 수행

  . 피 연산자가 실수면 실수형 연산 수행, 단 % 연산제 제외.


비트 연산의 특징

- left shift 연산자 (<<) : 대상 필드의 값을 2진 비트로 바꾼 후에 왼쪽으로 특정 비트 수만큼 이동 시키고 빈자리는 0으로 채우는 연산자이다.
- right shift 연산자(>>) : 대상 필드의 값을 2진수로 바꾼 후 오른쪽으로 특정 비트 수만큼 이동시키고 빈자리는 양수일때 0, 음수일때는 1로 채우는 연산자이다.

- 왼쪽으로의 비트 열 이동은 2의 배수의 곱

  예) 2<<2  : 2 X 2의 제곱 = 2 X 4 = 8

       1<<5 :  1 X 2의 5제곱 = 1 X 32 = 32

- 오른쪽으로의 비트 열 이동은 2배 배수의 나눗셈

  예) 8 >> 1 ; 8 / 2 = 4 출력

       8 >> 2  ; 8 / 4 = 2 출력



728x90

'안드로이드 > Java 문법' 카테고리의 다른 글

Java static 변수 이해하기  (0) 2017.10.20
[Java] 정보은닉과 캡슐화  (0) 2017.10.16
Java 클래스(Class) ★★★★★  (0) 2017.05.04
ArrayList and HashMap  (0) 2017.03.06
Java foreach 문  (0) 2017.03.01
블로그 이미지

Link2Me

,
728x90

Android Studio 에서 모듈을 새로 추가하는 순서를 이미지로 적어본다.




Application Name 을 적는다.





여기까지 하고 나면 관련 파일이 생성된다.




여기까지 했으면 이제 build.gradle 파일을 열어서 수정한다.




위 그림처럼 dependencies 에 추가하고 싶은 라이브러리를 추가해준다.

여기서 compileSdkVersion 정보는 수정하지 않아도 되고 필요한 버전으로 수정하면 된다.



마지막으로

를 눌러준다.


혹시라도 이런 메시지가 나오면 ....

AndroidManifest.xml 파일을 수정해준다.

android:roundIcon="@mipmap/ic_launcher_round"

이 한줄을 삭제하고 저장하면 다시 Sync Now 를 눌러주면 메시지가 없어진다.


이렇게 하면 모듈 추가가 완성된 것이라고 보면 된다.


compile 'org.jsoup:jsoup:1.10.3' 를 추가한 이유는 Web Site 파싱 관련 라이브러리 이기 때문이다.

https://jsoup.org/download 에 가면 최신버전 정보가 나온다.

Web Site 파싱 연습을 해보고자 한다.

가장 먼저 내 홈페이지 로그인 기능을 해보려고 한다.

728x90
블로그 이미지

Link2Me

,
728x90

안드로이드 스튜디오에서 MMS 보내는 기능을 테스트 해봤다.


MMS 직접 전송방식은 검색을 해보니 쉽게 구현할 수 있는게 아닌거 같다.

그래서 이 방식으로 보내는 테스트는 안해봤고 Intent.ACTION_SEND 방식으로 테스트를 했다.


이미지 첨부를 안한다면

sendIntent.putExtra(Intent.EXTRA_STREAM, Uri.parse(strAttachUrl));
sendIntent.setType("image/jpg");
두줄은 주석처리하면 된다.


Button mms_send = (Button) findViewById(R.id.btn_mms);
mms_send.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        String strPhone = "010-1234-5580";
        String strMessage = "전화주셔서 감사합니다.\n좋은 하루되세요.\n
            행복한 하루, 즐거운 하루, 즐거운 저녁\n
            행복하세요...\n행복은 마음에서..\n
            행복하세요...\n행복은 마음에서..\n
            행복하세요...\n행복은 마음에서..\n
            행복하세요...\n행복은 마음에서..\n
            행복하세요...\n행복은 마음에서..\n
            행복하세요...\n행복은 마음에서..";
        String strAttachUrl = "file://"+ Environment.getExternalStorageDirectory()+"/test.jpg";

        Uri uri = Uri.parse("file://"+ Environment.getExternalStorageDirectory()+"/test.jpg");
        Log.e("strAttachUrl : ", strAttachUrl);
        Log.e("imagePath : ", uri.getPath());

        Intent sendIntent = new Intent(Intent.ACTION_SEND);
        sendIntent.setClassName("com.android.mms", "com.android.mms.ui.ComposeMessageActivity");
        sendIntent.putExtra("address", strPhone);
        sendIntent.putExtra("sms_body", strMessage);
        sendIntent.putExtra(Intent.EXTRA_STREAM, Uri.parse(strAttachUrl));
        sendIntent.setType("image/jpg");
        sendIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        startActivity(sendIntent);
    }
});


로그인 처리 기능도 같이 넣어서 테스트를 하느라 AndroidManifest.xml 파일은 아래와 같다.

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.READ_CONTACTS" />
<uses-permission android:name="android.permission.SEND_MMS" />
<uses-permission android:name="android.permission.SEND_SMS" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />



앱 build.grale 에서 컴파일SDK 버전을 26으로 했더니 에러가 발생하면서 발송이 안된다.

카메라로 사진 촬영하여 발송하는 것처럼 이미지 첨부라서 안드로이드 7.0(Nougat) 이상에서 별도로 구현해 줘야 동작이 될 거 같다.

그래서 버전을 23으로 낮추고 테스트를 했더니 발송이 잘 된다.

apply plugin: 'com.android.application'

android {
    compileSdkVersion 23
    buildToolsVersion "25.0.0"

    defaultConfig {
        applicationId "com.tistory.link2me.login"
        minSdkVersion 19
        targetSdkVersion 23
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"

    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile 'com.android.support:support-v4:23.0.0'
    compile 'com.android.support:appcompat-v7:23.0.0'
    compile 'com.android.support.constraint:constraint-layout:1.0.2'
    compile 'com.squareup.okhttp3:okhttp:3.9.0'

}


안드로이드 7.0 이상 환경에서 컴파일하는 것도 테스트를 하게되면 적어둘 예정이다.

728x90
블로그 이미지

Link2Me

,
728x90

오디오 플레이어를 테스트 하다보니 화면 자동꺼짐 설정시간에 따라 음악 재생중에 화면이 꺼지면서 재생이 되지 않는 현상을 발견했다.


그래서 음악 재생중에는 화면 꺼짐이 발생하지 않게 처리하기 위한 코드를 추가했다.


@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_player);
    mContext = this.getBaseContext();

    // 화면 꺼짐 방지
    getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);

    // 재생할 목록 배열에 저장하기
    Intent intent = getIntent();
    list =(ArrayList<Song_Item>) intent.getSerializableExtra("playList");

    mediaPlayer = new MediaPlayer();

} 


위와 같이 한줄을 추가해주면 된다.


선택한 노래가 5곡인데 5곡이 모두 재생되고 나면 화면 꺼짐 방지 설정 때문에 아무런 조치를 안하면 화면이 계속 껴진 상태로 남아있더라.

그러므로 화면 해제 설정을 해주어야 한다.

getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);


화면 설정 해제 명령어를 넣어줬음에도 불구하고 화면(SCREEN)이 계속 ON 상태가 되는 현상이 있어서 Log를 찍어보니까 songProgressBar 핸들러 처리가 잘못되어 문제가 생겼다.

그래서 해당 코드를 수정하고 http://link2me.tistory.com/1350 내용을 업데이트 했다.


음악 재생이 완료되지 마자 자동꺼짐 설정 모드로 변경되지는 않는다.

만약 그렇게 하고 싶다면

@SuppressLint("Wakelock")
private void acquireCPUWakeLock(Context context) {
    // 잠든 화면 깨우기
    if (wakeLock != null) {
        return;
    }

    PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
    wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK | PowerManager.ACQUIRE_CAUSES_WAKEUP
            | PowerManager.ON_AFTER_RELEASE, "hi");
    wakeLock.acquire();
}

private void releaseCpuLock() {
    Log.e("PushWakeLock", "Releasing cpu WakeLock = " + wakeLock);

    if (wakeLock != null) {
        wakeLock.release();
        wakeLock = null;
    }
}


위 코드를 적절히 활용하면 될 것으로 본다.

mediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
    @Override
    public void onCompletion(MediaPlayer mp) {
        if(isRepeat){ // 반복재생
            playMusic(list.get(position));
        } else if(isShuffle){ // 랜덤재생
            Random rand = new Random();
            position = rand.nextInt((list.size() - 1) - 0 + 1) + 0;
            playMusic(list.get(position));
        } else {
            if(position + 1 < list.size()) {
                position++;
                playMusic(list.get(position));
            } else {
                Log.e("Music_Off","음악종료");
                seekHandler.removeCallbacks(mUpdateTimeTask);
                position = 0;
                acquireCPUWakeLock(mContext);
                releaseCpuLock();
                //getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
            }
        }

    }
});
 


728x90
블로그 이미지

Link2Me

,
728x90

ArrayList 로 선택한 노래만 팝업 메뉴로 만드는 방법을 구현하고 적어둔다.


팝업은 API 레벨 11 이상에서만 사용할 수 있다.
PopupMenu(Context context, View anchor)
- 두번째 인수는 팝업 메뉴를 표시할 앵커 뷰를 지정


popupMenu.getMenu().add(group_id, item_id, order, "노래타이틀");

popupMenu.getMenu().add(Menu.NONE, i, Menu.NONE, list.get(i).getTitle());

Menu.NONE 대신에 0 을 넣어도 된다.



//import android.support.v7.widget.PopupMenu; // ==> 이줄 추가하면 에러 발생함
import android.widget.PopupMenu;  // ==> 에러 발생 안함
import android.view.Menu;
import android.view.MenuItem;


 //import android.support.v7.widget.PopupMenu;
import android.widget.PopupMenu;
import android.view.Menu;
import android.view.MenuItem;

public class Player extends AppCompatActivity implements View.OnClickListener {
    private ImageButton btnPlaylist;

    private ArrayList<Song_Item> list;
    private MediaPlayer mediaPlayer;
    private int position = 0; // 현재 재생곡 위치

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_player);

        Intent intent = getIntent();
        list =(ArrayList<Song_Item>) intent.getSerializableExtra("playList");
        mediaPlayer = new MediaPlayer();

        // All player buttons
        btnPlaylist = (ImageButton) findViewById(R.id.btnPlaylist);

        btnPlaylist.setOnClickListener(this); // 곡 선택창

        System.out.println("선택된 노래곡수 = " + list.size());
        playMusic(list.get(position)); // 노래 곡
        seekBar_Progressing();
    }

    public void onClick(View v) {
        switch (v.getId()){
            case R.id.btnPlaylist:
                // 팝업 메뉴 나오게 하는 방법
                PopupMenu popupMenu = new PopupMenu(this, v);
                Menu menu = popupMenu.getMenu();
                for(int i=0; i < list.size();i++){

                    //Popup Menu에 들어갈 MenuItem 추가

                    menu.add(0,i,0,list.get(i).getTitle());
                }

                //메뉴가 클릭했을때 처리하는 부분

                popupMenu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() {
                    @Override
                    public boolean onMenuItemClick(MenuItem item) {
                        int i = item.getItemId();
                        Toast.makeText(Player.this, item.getTitle() + " 선택했습니다", Toast.LENGTH_SHORT).show();
                        playMusic(list.get(i)); // 노래 곡
                        return true;
                    }
                });
                popupMenu.show();
                break;
        }
    }

    public void playMusic(Song_Item song_item) {
        try {
            seek_bar.setProgress(0);
            songTitleLabel.setText(song_item.getTitle());
            mediaPlayer.reset();
            mediaPlayer.setDataSource(song_item.getDataPath());
            mediaPlayer.prepare();
            mediaPlayer.start(); // 노래 재생 시작

            seek_bar.setMax(mediaPlayer.getDuration()); // 음악파일의 전체길이
            seek_bar.setProgress(mediaPlayer.getCurrentPosition());
            if(mediaPlayer.isPlaying()){
                btnPlay.setVisibility(View.GONE);
                btnPause.setVisibility(View.VISIBLE);
            } else {
                btnPlay.setVisibility(View.VISIBLE);
                btnPause.setVisibility(View.GONE);
            }

            /* Album Art Bitmap을 얻는다. */
            final Uri artworkUri = Uri.parse("content://media/external/audio/albumart");
            ImageView mImgAlbumArt = (ImageView) findViewById(R.id.albumart);
            Uri albumArtUri = ContentUris.withAppendedId(artworkUri, song_item.getAlbumId());
            Picasso.with(Player.this)
                    .load(albumArtUri)
                    .resize(800,800)
                    .into(mImgAlbumArt);

        } catch (IOException e) {
            e.getMessage();
            Toast.makeText(Player.this, "Error!!", Toast.LENGTH_SHORT).show();
        }
    }
}



미리 정해진 xml 을 메뉴로 만드는 방법

https://www.tutlane.com/tutorial/android/android-popup-menu-with-examples


https://www.javatpoint.com/android-popup-menu-example 를 참조하면 도움된다.


728x90
블로그 이미지

Link2Me

,
728x90

안드로이드에서 이미지 파일 다운로드 소스 테스트한 것을 적어둔다.


이미지가 있는 url 에서 파일명만 추출하여 파일명을 자동으로 저장하기 위해서

private String getFileName(String url) {
    // url 에서 파일 이름만 자르기(확장자 제외)
   String fileName = url.substring( url.lastIndexOf('/')+1, url.length() );
   return fileName.substring(0, fileName.lastIndexOf('.'));
}

private String getFileName(String url) {
    // url 에서 파일만 자르기
   return url.substring( url.lastIndexOf('/')+1, url.length() );
}


이미지 파일을 저장할 폴더(디렉토리)를 생성하는 메소드

private File file, dir;
private String saveFolderName = "ImageTemp";

private void MakePhtoDir(){
    //saveFolderName = "/Android/data/" + getPackageName();
    //dir = new File(Environment.getExternalStorageDirectory().getAbsolutePath(), saveFolderName);
    dir = new File(Environment.getExternalStorageDirectory(), saveFolderName);
    if (!dir.exists())
        dir.mkdirs(); // make dir
}


이미지 파일은 다운로드 완료한 후에는 갤러리를 업데이트 해주어야 볼 수 있다.

sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, Uri.fromFile(file)));

이 한줄을 생략하면 USB 케이블 연결해서 해당 폴더를 확인해도 파일명을 다운로드 했는지 여부를 알 수가 없더라.

파일 다운로드 여부를 굳이 알 수 있게 할 것이 아니라면 생략해도 된다.


ImageView 에 출력하는 것은 아래 4줄만 적어주면 된다.

File file = new File(dir + "/" + tempFileName);
sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, Uri.fromFile(file)));
Bitmap photoBitmap = BitmapFactory.decodeFile(file.getAbsolutePath() );
imageView.setImageBitmap(photoBitmap);


갤러리에서 저장한 이미지를 확인하려면

//저장한 이미지 확인
Intent i = new Intent(Intent.ACTION_VIEW);
i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
i.setDataAndType(Uri.fromFile(file), "image/*"); //type 지정 (이미지)
getApplicationContext().startActivity(i);



전체 테스트 코드


Androidmanifest.xml 파일에 추가할 사항

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    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:orientation="vertical">

    <Button
        android:id="@+id/btn_photodownload"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:layout_marginTop="30dp"
        android:text="사진다운로드"
        tools:layout_editor_absoluteX="239dp"
        tools:layout_editor_absoluteY="31dp" />

    <ImageView
        android:id="@+id/DNImageView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:adjustViewBounds="true"
        android:layout_marginTop="30dp"
        android:background="@drawable/profile_noimage"
        android:src="@drawable/photo_base" />
</LinearLayout>

import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Environment;
import android.provider.MediaStore;
import android.view.View;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URL;
import java.net.URLConnection;

public class PhotoDownload extends Activity {
    private static String TAG = "Photo";
    TextView textView;
    ImageView imageView;
    Context context;

    ProgressBar progressBar;
    private File file, dir;
    private String savePath= "ImageTemp";
    private String FileName = null;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.photo_download);
        context = this.getBaseContext();

        MakePhtoDir();

        imageView = (ImageView) findViewById(R.id.DNImageView);

        Button photo_download = (Button) findViewById(R.id.btn_photodownload);
        photo_download.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                String imgUrl = "http://img.naver.net/static/www/u/2013/0731/nmms_224940510.gif";
                FileName = imgUrl.substring( imgUrl.lastIndexOf('/')+1, imgUrl.length() );
                DownloadPhotoFromURL downloadPhotoFromURL = new DownloadPhotoFromURL();
                // 동일한 파일이 있는지 검사
                if(new File(dir.getPath() + File.separator + FileName).exists() == false){
                    downloadPhotoFromURL.execute(imgUrl,FileName);
                } else {
                    Toast.makeText(context, "파일이 이미 존재합니다", Toast.LENGTH_SHORT).show();

                    File file = new File(dir + "/" + FileName);
                    Bitmap photoBitmap = BitmapFactory.decodeFile(file.getAbsolutePath() );
                    imageView.setImageBitmap(photoBitmap);
                }
            }
        });
    }

    private void MakePhtoDir(){
        //savePath = "/Android/data/" + getPackageName();
        //dir = new File(Environment.getExternalStorageDirectory().getAbsolutePath(), savePath);
        dir = new File(Environment.getExternalStorageDirectory(), savePath );
        if (!dir.exists())
            dir.mkdirs(); // make dir
    }

    public String getRealPathFromURI(Uri contentUri) {
        // 갤러리 이미지 파일의 실제 경로 구하기
        String[] proj = { MediaStore.Images.Media.DATA };
        Cursor cursor = getContentResolver().query(contentUri, proj, null, null, null);
        int column_index = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA);
        cursor.moveToFirst();
        return cursor.getString(column_index);
    }

    class DownloadPhotoFromURL extends AsyncTask<String, Integer, String> {
        int count;
        int lenghtOfFile = 0;
        InputStream input = null;
        OutputStream output = null;
        String tempFileName;

        @Override
        protected void onPreExecute() {
            super.onPreExecute();
            //progressBar.setProgress(0);
        }

        @Override
        protected String doInBackground(String... params) {
            tempFileName = params[1];
            file = new File(dir, params[1]); // 다운로드할 파일명
            try {
                URL url = new URL(params[0]);
                URLConnection connection = url.openConnection();
                connection.connect();

                lenghtOfFile = connection.getContentLength(); // 파일 크기를 가져옴

//                if (file.exists()) {
//                    file.delete();
//                    Log.d(TAG, "file deleted...");
//                }

                input = new BufferedInputStream(url.openStream());
                output = new FileOutputStream(file);
                byte data[] = new byte[1024];
                long total = 0;

                while ((count = input.read(data)) != -1) {
                    if (isCancelled()) {
                        input.close();
                        return String.valueOf(-1);
                    }
                    total = total + count;
                    if (lenghtOfFile > 0) { // 파일 총 크기가 0 보다 크면
                        publishProgress((int) (total * 100 / lenghtOfFile));
                    }
                    output.write(data, 0, count); // 파일에 데이터를 기록
                }

                output.flush();

            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                if (input != null) {
                    try {
                        input.close();
                    }
                    catch(IOException ioex) {
                    }
                }
                if (output != null) {
                    try {
                        output.close();
                    }
                    catch(IOException ioex) {
                    }
                }
            }
            return null;
        }

        protected void onProgressUpdate(Integer... progress) {
            super.onProgressUpdate(progress);
            // 백그라운드 작업의 진행상태를 표시하기 위해서 호출하는 메소드
            //progressBar.setProgress(progress[0]);
            //textView.setText("다운로드 : " + progress[0] + "%");
        }

        protected void onPostExecute(String result) {
            // pdLoading.dismiss();
            if (result == null) {
                Toast.makeText(getApplicationContext(), "다운로드 완료되었습니다.", Toast.LENGTH_LONG).show();

                File file = new File(dir + "/" + tempFileName);
                //이미지 스캔해서 갤러리 업데이트
                sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, Uri.fromFile(file)));
                Bitmap photoBitmap = BitmapFactory.decodeFile(file.getAbsolutePath() );
                imageView.setImageBitmap(photoBitmap);
            } else {
                Toast.makeText(getApplicationContext(), "다운로드 에러", Toast.LENGTH_LONG).show();
            }
        }
    }
}


728x90
블로그 이미지

Link2Me

,
728x90

이미지 파일 전송시 서버에서 구현할 PHP 소스는 다음과 같다.

개인 프로필 사진을 업로드할 경우의 수정사항이다.


<?php
if(isset($_POST['idx']) && $_POST['idx']>0){
    $idx=$_POST['idx'];

    $file_path='./photos/'.$idx.'.jpg'; //이미지화일명은 인덱스번호로 지정
    if(move_uploaded_file($_FILES['uploaded_file']['tmp_name'], $file_path)) {
        $result = array("result" => "success");
    } else{
        $result = array("result" => "error");
    }
    echo json_encode($result);
}
?>


안드로이드 수정사항

Button btn_UploadPhoto = (Button) findViewById(R.id.btn_Upload); // 사진 업로드
btn_UploadPhoto.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        if (TextUtils.isEmpty(imagePath)) {
            Toast.makeText(getApplicationContext(), "업로드할 사진을 선택해주세요", Toast.LENGTH_SHORT).show();
        } else {
            if (NetworkHelper.checkConnection(mContext)) { // 인터넷 연결 체크
                Log.d("Photo", "Photo Upload Task Start");
                String ImageUploadURL = Value.IPADDRESS + "/upload.php";
                SharedPreferences pref = getSharedPreferences("pref", Activity.MODE_PRIVATE);
                String idx = pref.getString("idx", "");
                ImageUploadTask imageUploadTask = new ImageUploadTask();
                imageUploadTask.execute(ImageUploadURL, imagePath, idx);
            } else {
                Toast.makeText(mContext, "인터넷 연결을 확인하세요", Toast.LENGTH_LONG).show();
            }
        }
    }
});


 class ImageUploadTask extends AsyncTask<String, Integer, Boolean> {
    ProgressDialog progressDialog; // API 26에서 deprecated

    @Override
    protected void onPreExecute() {
        super.onPreExecute();
        progressDialog = new ProgressDialog(MainActivity.this);
        progressDialog.setMessage("이미지 업로드중....");
        progressDialog.show();
    }

    @Override
    protected Boolean doInBackground(String... params) {
        try {
            JSONObject jsonObject = JSONParser.uploadImage(params[0],params[1],params[2]);
            if (jsonObject != null)
                return jsonObject.getString("result").equals("success");

        } catch (JSONException e) {
            Log.i("TAG", "Error : " + e.getLocalizedMessage());
        }
        return false;
    }

    @Override
    protected void onPostExecute(Boolean aBoolean) {
        super.onPostExecute(aBoolean);
        if (progressDialog != null)
            progressDialog.dismiss();

        if (aBoolean){
            Toast.makeText(getApplicationContext(), "파일 업로드 성공", Toast.LENGTH_LONG).show();
            // 임시 파일 삭제 (카메라로 사진 촬영한 이미지)
            if(mImageCaptureUri != null){
                File file = new File(mImageCaptureUri.getPath());
                if(file.exists()) {
                    file.delete();
                }
                mImageCaptureUri = null;
            }
            imagePath = null;
        }  else{
            Toast.makeText(getApplicationContext(), "파일 업로드 실패", Toast.LENGTH_LONG).show();
        }

    }
}

 public class JSONParser {

    public static JSONObject uploadImage(String imageUploadUrl, String sourceImageFile, String idx) {
        final MediaType MEDIA_TYPE_PNG = MediaType.parse("image/*");

        try {
            File sourceFile = new File(sourceImageFile);
            Log.d("TAG", "File...::::" + sourceFile + " : " + sourceFile.exists());
            String filename = sourceImageFile.substring(sourceImageFile.lastIndexOf("/")+1);

            // OKHTTP3
            RequestBody requestBody = new MultipartBody.Builder()
                    .setType(MultipartBody.FORM)
                    .addFormDataPart("uploaded_file", filename, RequestBody.create(MEDIA_TYPE_PNG, sourceFile))
                    .addFormDataPart("idx", idx)
                    .build();

            Request request = new Request.Builder()
                    .url(imageUploadUrl)
                    .post(requestBody)
                    .build();

            OkHttpClient client = new OkHttpClient();
            Response response = client.newCall(request).execute();
            if (response != null) {
                if (response.isSuccessful()) {
                    String res = response.body().string();
                    Log.e("TAG", "Success : " + res);
                    return new JSONObject(res);
                }
            }
        } catch (UnknownHostException | UnsupportedEncodingException e) {
            Log.e("TAG", "Error: " + e.getLocalizedMessage());
        } catch (Exception e) {
            Log.e("TAG", "Other Error: " + e.getLocalizedMessage());
        }
        return null;
    }
}


728x90
블로그 이미지

Link2Me

,
728x90

안드로이드 스튜디오에서 사진촬영 및 특정부분 이미지를 CROP 해서 업로드하는 법과 갤러리(앨범)에서 사진을 선택해서 업로드하는 걸 테스트하고 있다.


 삼성 갤럭시 S7 (7.0)

 사진 촬영후 CROP 전송 OK,

 갤러리 사진 선택후 CROP하면 기존 파일을 덮어쓰는 현상(해결 필요)

 삼성 갤럭시 A5 (6.0.1)

 사진 촬영후 CROP 전송 OK, 갤러리 CROP 후 전송 OK

 삼성 갤럭시 S4 (5.0.1)

 사진 촬영후 CROP 전송 OK, 갤러리 CROP 후 전송 OK


안드로이드 7.0 이상 운영체제에서 문제가 있는 것인지 집중적인 테스트가 필요할 거 같다.

지난번 테스트보다는 기능이 개선되었지만, 좀 더 보완이 필요하다.

7.0 에서 테스트한 결과는 이미지를 축소 저장하는 saveCropImage 메소드가 없이 자체적으로(?) 이미지를 축소하기 때문에 원본 갤러리 파일에 문제가 생기는 거 같다.

사진 촬영후 전송할 경우 같은 myphoto.jpg 로 저장하도록 해서 문제 없이 서버에 축소된 이미지가 업로드된다.

문제는 갤러리 사진을 업로드할 경우 CROP 까지는 잘되는데, CROP 한 후에 원본 이미지를 훼손하는 문제가 생긴다. 갤럭시 S4, A5에서는 정상동작한다. 코드 상으로도 정상 동작되어야 맞는거 같은데.....

7.0 에서 뭔가 내가 놓치는 것이 뭘까??? 좀 더 찾아보고 해결해보련다.


음.... 확인해보니 Build.VERSION.SDK_INT 가 24이상인 경우에는 다른 방법으로 구현해야 하더라.

일일이 로그를 찍어가면서 확인해보니, CROP 이후에 값 자체가 넘어오지 않고 그냥 임시로 생성한 파일만 지정한 폴더(디렉토리)에 저장되는 걸 확인했다.

내가 구현한 방식은 한마디로 편법으로 7.0 에서 동작되었던 것이다.

임시로 저장된 파일을 다시 읽어서 그 파일을 서버로 업로드하는 거다.


 package com.tistory.link2me.imageupload;

import android.Manifest;
import android.app.Activity;
import android.app.ActivityManager;
import android.app.AlertDialog;
import android.app.ProgressDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.provider.MediaStore;
import android.provider.Settings;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat;
import android.text.TextUtils;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.Toast;

import com.squareup.picasso.Picasso;
import com.tistory.link2me.common.BackPressHandler;
import com.tistory.link2me.common.JSONParser;
import com.tistory.link2me.common.NetworkHelper;

import org.json.JSONException;
import org.json.JSONObject;

import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

public class PhotoActivity extends Activity {
    Context mContext;

    private static final int PICK_FROM_CAMERA = 0; //카메라 촬영으로 사진 가져오기
    private static final int PICK_FROM_ALBUM = 1; //앨범에서 사진 가져오기
    private static final int CROP_FROM_CAMERA = 2; // 가져온 사진을 자르기 위한 변수
    private static final int PICK_FROM_File = 3; //앨범에서 사진 가져오기(원본 올리기)

    private static Uri mImageCaptureUri; // Stactic 으로 설정해야 에러 발생하지 않음.
    private String imagePath;
    private ImageView imageView;
    private Bitmap photoBitmap;
    String saveFolderName = "cameraTemp";
    File mediaFile = null;
    String saveFileName = "myphoto.jpg";

    ProgressDialog mProgressDialog;
    private BackPressHandler backPressHandler;

    private String[] permissions = {
            Manifest.permission.READ_EXTERNAL_STORAGE,
            Manifest.permission.WRITE_EXTERNAL_STORAGE,
            Manifest.permission.CAMERA,
            Manifest.permission.RECORD_AUDIO
    };
    private static final int MULTIPLE_PERMISSIONS = 101;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_photo);
        mContext = this.getBaseContext();
        backPressHandler = new BackPressHandler(this); // 뒤로 가기 버튼 이벤트

        System.out.println("프로세스당 메모리 용량 : " + ((ActivityManager)getSystemService(Context.ACTIVITY_SERVICE)).getMemoryClass());
        System.out.println("largeHeap 크기 : " + ((ActivityManager)getSystemService(Context.ACTIVITY_SERVICE)).getLargeMemoryClass());
        System.out.println("패키지 설치 경로 : " + getApplicationContext().getFilesDir().getAbsolutePath().replace("files", ""));

        checkPermissions();
        initView();
    }

    private void initView() {
        imageView = (ImageView) findViewById(R.id.croppedImageView);
        imageView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                AlertDialog.Builder builder = new AlertDialog.Builder(PhotoActivity.this);
                builder.setMessage("업로드할 이미지 선택");
                DialogInterface.OnClickListener cameraListener = new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                        doTakePhotoAction();
                    }
                };

                DialogInterface.OnClickListener albumListener = new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                        getGallery(); // 갤러리(앨범)에서 이미지 가져오기
                    }
                };

                DialogInterface.OnClickListener cancelListener = new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                        dialog.dismiss();
                    }

                };
                builder.setTitle("업로드할 이미지 선택");
                builder.setPositiveButton("사진촬영", cameraListener);
                builder.setNeutralButton("앨범선택", albumListener);
                builder.setNegativeButton("취소", cancelListener);
                builder.show();

            }
        });

        Button btn_UploadPhoto = (Button) findViewById(R.id.btn_Upload);
        btn_UploadPhoto.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                if (TextUtils.isEmpty(imagePath)) {
                    Toast.makeText(getApplicationContext(), "업로드할 사진을 선택해주세요", Toast.LENGTH_SHORT).show();
                } else {
                    if (NetworkHelper.checkConnection(mContext)) { // 인터넷 연결 체크
                        Log.d("Photo", "Photo Upload Task Start");
                        String ImageUploadURL = Value.IPADDRESS + "/upload.php";
                        ImageUploadTask imageUploadTask = new ImageUploadTask();
                        imageUploadTask.execute(ImageUploadURL, imagePath);
                    } else {
                        Toast.makeText(mContext, "인터넷 연결을 확인하세요", Toast.LENGTH_LONG).show();
                    }
                }
            }
        });

    }

    private Boolean isAirModeOn() {
        Boolean isAirplaneMode;
        if(Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1){
            isAirplaneMode = Settings.System.getInt(getContentResolver(),
                    Settings.System.AIRPLANE_MODE_ON, 0) == 1;
        }else{
            isAirplaneMode = Settings.Global.getInt(getContentResolver(),
                    Settings.Global.AIRPLANE_MODE_ON, 0) == 1;
        }
        return isAirplaneMode;
    }

    /**
     * 카메라에서 사진 촬영
     */
    private void doTakePhotoAction() {  // 카메라 앱을 이용하여 사진 찍기
        // Intent를 사용하여 안드로이드에서 기본적으로 제공해주는 카메라를 이용하는 방법 이용
        Intent cameraIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
        // 이미지가 저장될 파일은 카메라 앱이 구동되기 전에 세팅해서 넘겨준다.
        cameraIntent.putExtra(MediaStore.EXTRA_OUTPUT, getImageUri(saveFileName)); // null
        cameraIntent.putExtra("return-data", true);
        startActivityForResult(cameraIntent, PICK_FROM_CAMERA); // 7.0 에서는 에러가 발생함. (API26 으로 컴파일 한 경우)
    }

    // 사진 선택을 위해 갤러리를 호출
    private void getGallery() {
        // File System.
        final Intent galleryIntent = new Intent();
        galleryIntent.setType("image/*"); // 이미지 파일 호출
        galleryIntent.setAction(Intent.ACTION_PICK);

        // Chooser of file system options.
        final Intent chooserIntent = Intent.createChooser(galleryIntent, "Select Image");
        galleryIntent.setType(MediaStore.Images.Media.CONTENT_TYPE);
        //startActivityForResult(chooserIntent, PICK_FROM_File);
        startActivityForResult(chooserIntent, PICK_FROM_ALBUM);
    }

    private Uri getImageUri(String saveFile) {
        // 임시로 사용할 파일의 경로를 생성
        File mediaStorageDir = new File(Environment.getExternalStorageDirectory() + "/DCIM", saveFolderName);
        if (! mediaStorageDir.exists()){
            if (! mediaStorageDir.mkdirs()){
                Log.d("MyCameraApp", "failed to create directory");
                return null;
            }
        }

        String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date());
        if(saveFile != null){
            mediaFile = new File(mediaStorageDir.getPath() + File.separator + saveFile);
        } else {
            mediaFile = new File(mediaStorageDir.getPath() + File.separator + "pic_"+ timeStamp + ".jpg");
        }
        mImageCaptureUri = Uri.fromFile(mediaFile);
        imagePath = mImageCaptureUri.getPath();
        Log.e("mImageCaptureUri : ", mImageCaptureUri.toString());
        Log.e("imagePath : ", imagePath);

        return mImageCaptureUri;
    }

    public String getRealPathFromURI(Uri contentUri) {
        String[] proj = { MediaStore.Images.Media.DATA };
        Cursor cursor = getContentResolver().query(contentUri, proj, null, null, null);
        int column_index = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA);
        cursor.moveToFirst();
        return cursor.getString(column_index);
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        if(resultCode != Activity.RESULT_OK) return;
        switch (requestCode){
            case PICK_FROM_ALBUM: {
                mImageCaptureUri = data.getData();
                Log.e("앨범이미지 CROP",mImageCaptureUri.getPath().toString());
                imagePath = getRealPathFromURI(mImageCaptureUri); // 실제 파일이 존재하는 경로
                Log.e("앨범이미지 경로",imagePath);
            }

            case PICK_FROM_CAMERA: {

                // 이미지를 가져온 이후의 리사이즈할 이미지 크기를 결정
                Intent intent = new Intent("com.android.camera.action.CROP");
                intent.setDataAndType(mImageCaptureUri, "image/*");

                // CROP할 이미지를 125*170 크기로 저장
                intent.putExtra("outputX", 125); // CROP한 이미지의 x축 크기
                intent.putExtra("outputY", 170); // CROP한 이미지의 y축 크기
                intent.putExtra("aspectX", 1); // CROP 박스의 X축 비율
                intent.putExtra("aspectY", 1); // CROP 박스의 Y축 비율
                intent.putExtra("scale", true);
                intent.putExtra("return-data", true);

                startActivityForResult(intent, CROP_FROM_CAMERA); // CROP_FROM_CAMERA case문 이동
                break;
            }

            case CROP_FROM_CAMERA:  {
                // CROP 된 이후의 이미지를 넘겨 받음
                final Bundle extras = data.getExtras();

                // CROP 된 이미지를 저장하기 위한 File 경로 설정
                String filePath = getImageUri(saveFileName).getPath();
                Log.e("mImageCaptureUri : ", "Croped " + mImageCaptureUri.toString());

                imagePath = filePath;

                if(extras != null) {
                    //photoBitmap = extras.getParcelable("data"); // CROP된 BITMAP
                    photoBitmap = (Bitmap)data.getExtras().get("data"); // CROP된 BITMAP
                    // 레이아웃의 이미지칸에 CROP된 BITMAP을 보여줌
                    saveCropImage(photoBitmap,imagePath); //  CROP 된 이미지를 외부저장소, 앨범에 저장한다.
                    // sendBroadcast를 통해 Crop된 사진을 앨범에 보이도록 갱신한다.
                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
                        sendBroadcast(new Intent( Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, Uri.fromFile(mediaFile)) );
                    } else {
                        sendBroadcast(new Intent(Intent.ACTION_MEDIA_MOUNTED, Uri.parse("file://"+ Environment.getExternalStorageDirectory())));
                    }
                }

                Picasso.with(mContext).load(new File(imagePath))
                        .into(imageView); // 피카소 라이브러를 이용하여 선택한 이미지를 imageView에 출력

                break;
            }
            case PICK_FROM_File: {
                // URI 정보를 이용하여 이미지(사진) 정보를 가져온다.
                if (data == null) {
                    Toast.makeText(mContext, "Unable to Pickup Image", Toast.LENGTH_SHORT).show();
                    return;
                }
                Uri selectedImageUri = data.getData();
                Log.e("IMAGE SEL", "" + selectedImageUri);
                String[] filePathColumn = {MediaStore.Images.Media.DATA};

                Cursor cursor = getContentResolver().query(selectedImageUri, filePathColumn, null, null, null);

                if (cursor != null) {
                    cursor.moveToFirst();

                    imagePath = cursor.getString(cursor.getColumnIndex(filePathColumn[0]));
                    Log.e("IMAGE SEL imagePath : ", imagePath);                 

                    Picasso.with(mContext).load(new File(imagePath))
                            .into(imageView); // 피카소 라이브러를 이용하여 선택한 이미지를 imageView에 출력
                    cursor.close();

                }

                break;
            }
        }

    }

    // Bitmap 을 저장하는 메소드
    private void saveCropImage(Bitmap bitmap, String filePath) {
        //read image file
        File copyFile = new File(filePath);
        BufferedOutputStream bos = null;
        try {
            copyFile.createNewFile();
            int quality = 100;
            bos = new BufferedOutputStream(new FileOutputStream(copyFile));
            bitmap.compress(Bitmap.CompressFormat.JPEG, quality, bos);
            // 이미지가 클 경우 OutOfMemoryException 발생이 예상되어 압축
            bos.flush();
            bos.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    class ImageUploadTask extends AsyncTask<String, Integer, Boolean> {
        ProgressDialog progressDialog; // API 26에서 deprecated

        @Override
        protected void onPreExecute() {
            super.onPreExecute();
            progressDialog = new ProgressDialog(PhotoActivity.this);
            progressDialog.setMessage("이미지 업로드중....");
            progressDialog.show();
        }

        @Override
        protected Boolean doInBackground(String... params) {
            try {
                JSONObject jsonObject = JSONParser.uploadImage(params[0],params[1]);
                if (jsonObject != null)
                    return jsonObject.getString("result").equals("success");

            } catch (JSONException e) {
                Log.i("TAG", "Error : " + e.getLocalizedMessage());
            }
            return false;
        }

        @Override
        protected void onPostExecute(Boolean aBoolean) {
            super.onPostExecute(aBoolean);
            if (progressDialog != null)
                progressDialog.dismiss();

            if (aBoolean){
                Toast.makeText(getApplicationContext(), "파일 업로드 성공", Toast.LENGTH_LONG).show();
            }  else{
                Toast.makeText(getApplicationContext(), "파일 업로드 실패", Toast.LENGTH_LONG).show();
            }

            // 임시 파일 삭제 (카메라로 사진 촬영한 이미지)
            if(mImageCaptureUri != null){
                File file = new File(mImageCaptureUri.getPath());
                if(file.exists()) {
                    file.delete();
                }
                mImageCaptureUri = null;
            }

            if (aBoolean){
                imagePath = null;
            }
            
        }

    }

    @Override
    public void onBackPressed() {
        backPressHandler.onBackPressed();
    }

    public boolean checkPermissions() {
        int result;
        List<String> permissionList = new ArrayList<>();
        for (String pm : permissions) {
            result = ContextCompat.checkSelfPermission(this, pm);
            if (result != PackageManager.PERMISSION_GRANTED) {
                permissionList.add(pm);
            }
        }
        if (!permissionList.isEmpty()) {
            ActivityCompat.requestPermissions(this, permissionList.toArray(new String[permissionList.size()]), MULTIPLE_PERMISSIONS);
            return false;
        }
        return true;
    }

    public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) {
        switch (requestCode) {
            case MULTIPLE_PERMISSIONS: {
                if (grantResults.length > 0) {
                    for (int i = 0; i < permissions.length; i++) {
                        if (permissions[i].equals(this.permissions[i])) {
                            if (grantResults[i] != PackageManager.PERMISSION_GRANTED) {
                                showToast_PermissionDeny();
                            }
                        }
                    }
                } else {
                    showToast_PermissionDeny();
                }
                return;
            }
        }
    }

    public void showToast_PermissionDeny() {
        Toast.makeText(this, "권한 요청에 동의 해주셔야 이용 가능합니다. 설정에서 권한 허용 하시기 바랍니다.", Toast.LENGTH_SHORT).show();
        finish();
    }
}


앱 build.Gradle 설정 내용

apply plugin: 'com.android.application'

android {
    compileSdkVersion 22
    buildToolsVersion "25.0.3"

    defaultConfig {
        applicationId "com.tistory.link2me.photoupload"
        minSdkVersion 19
        targetSdkVersion 22
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"

    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
        exclude group: 'com.android.support', module: 'support-annotations'
    })
    compile 'com.android.support:appcompat-v7:22.0.0'
    compile 'com.android.support.constraint:constraint-layout:1.0.2'
    compile 'com.squareup.okhttp3:okhttp:3.9.0'
    compile 'com.squareup.picasso:picasso:2.5.2'
    compile 'com.google.firebase:firebase-messaging:10.2.6'
    testCompile 'junit:junit:4.12'
}


public class NetworkHelper {

    /** CHECK WHETHER INTERNET CONNECTION IS AVAILABLE OR NOT */
    public static boolean checkConnection(Context context) {
        return  ((ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE)).getActiveNetworkInfo() != null;
    }
}

 public class OKHttpComm {
    //GET network request
    //@RequiresApi(api = Build.VERSION_CODES.KITKAT)
    public static String GET(String url) throws IOException {
        OkHttpClient client = new OkHttpClient();
        Request request = new Request.Builder()
                .url(url)
                .build();

        Response response = null;
        try {
            response = client.newCall(request).execute();
            return response.body().string();
        } catch (IOException e) {
            Log.e("OKHttp", "error in getting response get request okhttp");
        }
        return null;
    }

    //POST network request
    public static String POST(String url, RequestBody body) throws IOException {
        OkHttpClient client = new OkHttpClient();
        Request request = new Request.Builder()
                .url(url)
                .post(body)
                .build();
        Response response = client.newCall(request).execute();
        return response.body().string();
    }   
}

 public class PHPComm extends Activity {

    // serverURL : JSON 요청을 받는 서버의 URL
    // postParams : POST 방식으로 전달될 입력 데이터
    // 반환 데이터 : 서버에서 전달된 JSON 데이터
    public static String getJson(String serverUrl, String postParams) throws Exception {

        BufferedReader bufferedReader = null;  
        try {  
            //Thread.sleep(100);
            URL url = new URL(serverUrl);  
            HttpURLConnection conn = (HttpURLConnection) url.openConnection();
            // 세션 쿠키 전달
            String cookieString = CookieManager.getInstance().getCookie(Value.IPADDRESS);
            
            StringBuilder sb = new StringBuilder();  

            if(conn != null){ // 연결되었으면
                //add request header
                conn.setRequestMethod("POST");
                conn.setRequestProperty("USER-AGENT", "Mozilla/5.0");
                conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
                conn.setRequestProperty("Accept-Language", "en-US,en;q=0.5");
                if (cookieString != null) {
                   conn.setRequestProperty("Cookie", cookieString);
                   Log.e("PHP_getCookie", cookieString);
                 }
                conn.setConnectTimeout(10000);
                conn.setReadTimeout(10000);
                conn.setUseCaches(false);
                conn.setDefaultUseCaches(false);
                conn.setDoOutput(true); // POST 로 데이터를 넘겨주겠다는 옵션
                conn.setDoInput(true);

                // Send post request
                DataOutputStream wr = new DataOutputStream(conn.getOutputStream());
                wr.writeBytes(postParams);
                wr.flush();
                wr.close();

                int responseCode = conn.getResponseCode();
                System.out.println("GET Response Code : " + responseCode);        
                if(responseCode == HttpURLConnection.HTTP_OK){ // 연결 코드가 리턴되면
                    bufferedReader = new BufferedReader(new InputStreamReader(conn.getInputStream()));
                    String json;
                    while((json = bufferedReader.readLine())!= null){
                        sb.append(json + "\n");
                    }      
                }
                bufferedReader.close();
            }
            System.out.println("PHP Comm Out Size : " + sb.length());
            return sb.toString().trim();  
            // 수행이 끝나고 리턴하는 값은 다음에 수행될 onProgressUpdate 의 파라미터가 된다
        } catch(Exception e){  
            return new String("Exception: " + e.getMessage());
        }   
 
    }

}



2017.9.20 수정 사항

안드로이드 7.0 (삼성 갤럭시 기준)에서 이미지 저장하는 것이 기존 것과 달라서 다른 방법으로 시도를 해봤지만 compileSdkVersion 22 로 해서인지 다른 블로그에서 해보라는 방식으로 해도 안된다.

수정 테스트한 것만 발췌하여 적어둔다.




2017.9.21 LG LGM-X600K 테스트

LG폰을 구해서 사진 촬영 및 이미지 업로드 테스트를 했더니 기존 소스 코드로 해야만 동작이 되더라.

그래서 아래 부분을 보완했다.

대부분 KIKAT 이상 단말이므로 sendBroadcast(new Intent( Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, Uri.fromFile(mediaFile)) ); 부분만 코드에 추가해도 될 것으로 본다.


case CROP_FROM_CAMERA:  {
    // CROP 된 이후의 이미지를 넘겨 받음
    final Bundle extras = data.getExtras();

    // CROP 된 이미지를 저장하기 위한 File 경로 설정
    String filePath = getFileUri(saveFileName).getPath();
    imagePath = filePath;

    if(extras != null) {
        //photoBitmap = extras.getParcelable("data"); // CROP된 BITMAP
        photoBitmap = (Bitmap)data.getExtras().get("data"); // CROP된 BITMAP
        saveCropImage(photoBitmap,filePath); //  CROP 된 이미지를 외부저장소, 앨범에 저장한다.
        // sendBroadcast를 통해 Crop된 사진을 앨범에 보이도록 갱신한다.
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            sendBroadcast(new Intent( Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, Uri.fromFile(mediaFile)) );
        } else {
            sendBroadcast(new Intent(Intent.ACTION_MEDIA_MOUNTED, Uri.parse("file://"+ Environment.getExternalStorageDirectory())));
        }
    }

    photoBitmap = BitmapFactory.decodeFile(imagePath );
    // 레이아웃의 이미지칸에 CROP된 BITMAP을 보여줌
    imageView.setImageBitmap(photoBitmap);

    break;
}


728x90
블로그 이미지

Link2Me

,
728x90

사진을 촬영하여 이미지를 업로드하기 위한 기능을 테스트하려고 한다.

사진 촬영하여 CROP 하는 곳에서 제대로 처리를 못하고 있어서 라이브러리를 이용해보려고 한다.


안드로이드 스튜디오에서 앱 build.Gradle 을 아래와 같이 수정했더니 에러가 발생한다.


 apply plugin: 'com.android.application'

android {
    compileSdkVersion 22
    buildToolsVersion "25.0.0"

    defaultConfig {
        applicationId "com.tistory.link2me.imageupload"
        minSdkVersion 16
        targetSdkVersion 22
        versionCode 1
        versionName "1.0"

    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile 'com.android.support:support-v4:22.0.0'
    compile 'com.android.support:appcompat-v7:22.0.0'
    compile 'com.android.support.constraint:constraint-layout:1.0.2'
    compile 'com.squareup.okhttp3:okhttp:3.9.0'
    compile 'com.squareup.picasso:picasso:2.5.2'
    compile 'com.google.firebase:firebase-messaging:10.2.6'
    compile 'com.isseiaoki:simplecropview:1.1.6'
}


roundIcon이 API 25에서 추가된 것이라 에러가 발생한다.

AndroidMenifest.xml 파일 내에 있는 android:roundIcon="@mipmap/ic_launcher"
를 제거해주고 Clean Project를 하면 에러가 사라진다.


AppCompatActivity는 안드로이드 하위버전을 지원하는 액티비티다.
Support Library에 있는 클래스들은 안드로이드 하위버전을 지원하기 위해 존재한다.
안드로이드 4.0 이상부터 지원하겠다고 하면 AppCompatActivity을 쓸 이유가 없다. 그냥 Activity를 쓰면 된다.

public class MainActivity extends AppCompatActivity {

public class MainActivity extends Activity {
로 변경했다.


2017.9.21

- simplecropview 라이브러리를 추가해서 CROP 하는 것은 실패했다.

  구글, 블로그에 나온 방법으로 코드를 구현하여 해결했다.

728x90
블로그 이미지

Link2Me

,
728x90

OKHttp 라이브러리를 이용하여 간단하게 POST 로 데이터를 전송할 경우 알아두어야 할 사항을 적어둔다.

OKHttp 라이브러리 사용조건이 최소 KIKAT 버전 이상인 거 같더라. (확실한 것은 더 테스트를 해봐야....)


RequestBody 객체 부분이 파일을 첨부하는 경우와 일반 텍스트를 전송하는 경우가 약간 다르다.

나머지 부분은 동일하다.  파일을 업로드하는 경우에는 OKHttp 라이브러리를 사용하는 것이 HttpURLConnection 으로 하는 것보다 코드도 심플하고 잘 동작한다.


httpClient 는 Handler를 사용하지 않는 곳에서도 동작이 되는데, OKHttpClient는 AsyncTask 또는 Handler 에서만 동작되더라.

로그인 버튼을 누르면 httpClient 가 동작되도록 되어 있는 코드를 그냥 단순하게 OKHttp 라이브러리를 사용해서 변환하면 동작이 안된다.


httpClient = new DefaultHttpClient();
localContext = new BasicHttpContext();
httpPost = new HttpPost(Value.IPADDRESS + "/loginChk.php");

entity = new MultipartEntity(HttpMultipartMode.BROWSER_COMPATIBLE);

try {
    entity.addPart("loginID", new StringBody(Value.encrypt(ID)));
    entity.addPart("loginPW", new StringBody(Value.encrypt(PW)));
} catch (UnsupportedEncodingException e1) {
    e1.printStackTrace();
}

httpPost.setEntity(entity);
try {
    response = httpClient.execute(httpPost, localContext);
} catch (ClientProtocolException e1) {
    e1.printStackTrace();
} catch (IOException e1) {
    e1.printStackTrace();
}
try {
    reader = new BufferedReader(new InputStreamReader(response.getEntity().getContent(), "UTF-8"));
} catch (UnsupportedEncodingException e1) {
    e1.printStackTrace();
} catch (IllegalStateException e1) {
    e1.printStackTrace();
} catch (IOException e1) {
    e1.printStackTrace();
}

try {
    Response = reader.readLine();
    Log.d(TAG, "응답 ====" + Response);
} catch (IOException e1) {
    e1.printStackTrace();
}


위 코드는 Eclipse 기반의 minSdkVersion 8에서 동작되는 코드인데 Android Studio minSdkVersion 16 이상으로 변환하고 FCM을 연동하니까 동작이 안되더라. FCM(Firebase Cloud Message)는 테스트를 하다보니 minSdkVersion 10 인거 같더라.

HttpClient 는 Android SDK API 23 부터는 더이상 지원을 하지 않는다.

앱 Gradle에서 compileSdkVersion 23으로 한 경우에는 useLibrary 'org.apache.http.legacy'를 하라고 나온다.

내가 테스트한 환경은 compileSdkVersion 22로 했다. 퍼미션 지정하는 것이 귀찮기도 하고 Eclipse 툴로 컴파일 한 것이 잘 동작하는 걸 Android Studio 기반으로 다시 코드를 수정 개발해야 하는 상황이라 수정을 조금이라도 덜 하는 방법을 찾아보려고 했다.

구글링을 해보면 HttpClient 를 사용할 수 있는 방법이 나오는데 그 방법으로 하는데 에러가 나면서 문제가 생긴다.

그래서 코드를 수정 사용해야 하는데 AsyncTask 기반의 OKHttp 또는 HttpURLConnection 로 변환해야 정상 동작된다.


PHPComm.getJson(params[0], params[1]) 코드는 http://link2me.tistory.com/1247 를 참조하면 된다.

OKHttp 로그인 예제 http://link2me.tistory.com/1353 를 보면서 이 내용을 보면 이해할 수 있다.


Uri.Builder builder = new Uri.Builder()
    .appendQueryParameter("loginID", Value.encrypt(ID))
    .appendQueryParameter("loginPW", Value.encrypt(PW));
String postParams = builder.build().getEncodedQuery();
AsyncTaskLogin asyncLogin = new AsyncTaskLogin();
asyncLogin.execute(Value.IPADDRESS + "/loginChk.php", postParams);

 class AsyncTaskLogin extends AsyncTask<String, Void, String> {

    @Override
    protected String doInBackground(String... params) {
        // 이곳에 OKHttp 라이브러리 기반 코드를 작성
        // 또는 HttpURLConnection 코드를 작성한다.
        try {
            return PHPComm.getJson(params[0], params[1]);
        } catch (Exception e) {
            return new String("Exception: " + e.getMessage());
        }
    }

    protected void onPostExecute(String result) {

    }
}


HttpClient 를 사용하는 방법이 http://goodnut.tistory.com/60 에 나오는데 이 방법으로는 테스트를 안해봤다.


아래 코드는 단독으로는 동작이 안되고 에러가 발생한다.

반드시 Handler or AsyncTask 에서 동작하도록 코드를 작성해야 한다.


void Agree() {
    String url = Value.IPADDRESS + "/regAssent.php";

    RequestBody requestBody = new FormBody.Builder()
            .add("idx", idx)
            .add("assent", "1")
            .build();

    Request request = new Request.Builder()
            .url(url)
            .post(requestBody)
            .build();

    OkHttpClient client = new OkHttpClient();
    try {
        Response response = client.newCall(request).execute();
        Log.d(TAG, "응답 ====" + response);
    } catch (IOException e) {
        e.printStackTrace();
    }
}
 


참고하면 도움되는 사이트


http://www.vogella.com/tutorials/JavaLibrary-OkHttp/article.html


http://www.zoftino.com/android-okhttp-example


728x90
블로그 이미지

Link2Me

,
728x90

배터리 교체 등으로 스마트폰의 전원을 OFF 후 부팅시 특정 앱이 자동으로 실행되도록 하는 코드다.


AndroidManifest.xml 파일에 추가할 사항

<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />

<receiver
    android:name=".AutoRun"
    android:enabled="true"
    android:exported="false">
    <intent-filter>
        <action android:name="android.intent.action.BOOT_COMPLETED"/>
    </intent-filter>
</receiver>


android:enabled="true" : 시스템이 알아서 AutoRun Receiver를 실행한다.
android:exported="false" : 외부 어플리케이션에서는 사용할 수 없다.


AutoRun 브로드캐스트 리시버를 생성한다.

Intent intent = new Intent(context, Intro.class); // 부팅후 자동실행할 Activity 는 Intro.java 파일


import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;

public class AutoRun extends BroadcastReceiver {

    @Override
    public void onReceive(Context context, Intent intent) {
        // 배터리 교체 등으로 부팅시 앱 자동실행 시키기
        String action = intent.getAction();
        if(action.equals("android.intent.action.BOOT_COMPLETED")){
            Intent ii = new Intent(context, Intro.class);
           
ii.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
            context.startActivity(
ii);
        }
    }
}
 




728x90
블로그 이미지

Link2Me

,
728x90

안드로이드 어플 사용자에게 PUSH 메시지를 발송하기 위해서는 PHP 서버에서 관련 코드를 구현해야 한다.

앱이 백그라운드에서 실행중이거나 앱이 죽어있을 때에도 동작하게 하려면 FCM API를 직접 호출해주는 방식을 사용해야 한다.


api url :: https://fcm.googleapis.com/fcm/send


https://console.firebase.google.com/ 에 접속한다.





서버키 4번을 복사해서 아래 코드에 붙여넣기를 한다.


=== fcmpush.php ====

 <?php
require_once 'dbconnect.php'; // db접속
//데이터베이스에 접속해서 토큰들을 가져와서 FCM에 발신요청
$sql = "select tokenID From member_data where LENGTH(tokenID)>63";
$result = mysqli_query($dbconn,$sql);
$tokens = array();
if(mysqli_num_rows($result) > 0 ){
    while ($row = mysqli_fetch_assoc($result)) {
        $tokens[] = $row['tokenID'];
    }
} else {
    echo 'There are no Transfer Datas';
    exit;
}

mysqli_close($dbconn);

$title = isset($_POST['title'])? $_POST['title'] : "PUSH TEST";
$message = isset($_POST['message'])? $_POST['message'] : "새글이 등록되었습니다";

$arr = array();
$arr['title'] = $title;
$arr['message'] = $message;

$message_status = Android_Push($tokens, $arr);
//echo $message_status;
// 푸시 전송 결과 반환.
$obj = json_decode($message_status);

// 푸시 전송시 성공 수량 반환.
$cnt = $obj->{"success"};

echo $cnt;

function Android_Push($tokens, $message) {
    $url = 'https://fcm.googleapis.com/fcm/send';
    $apiKey = "서버키";

    $fields = array('registration_ids' => $tokens,'data' => $message);
    $headers = array('Authorization:key='.$apiKey,'Content-Type: application/json');

    $ch = curl_init();
    curl_setopt($ch, CURLOPT_URL, $url);
    curl_setopt($ch, CURLOPT_POST, true);
    curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt ($ch, CURLOPT_SSL_VERIFYHOST, 0);
    curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
    curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($fields));
    $result = curl_exec($ch);
    if ($result === FALSE) {
        die('Curl failed: ' . curl_error($ch));
    }
    curl_close($ch);
    return $result;
}
?>


=== fcmsender.php 샘플 파일 ===

<!DOCTYPE html>
<head>
<title>FCM Sender</title>
    <meta charset="utf-8" />
</head>
<body>
    <form action="fcmpush.php" method="post">
        <table style="width:100;padding:0;margin:0px;border:none">
        <tr>
            <th style="width:20%">제목</th>
            <td style="width:80%"><input type="text" name="title" value="" style="width:100%"></td>
        </tr>
        <tr>
            <th>내용</th>
            <td>
            <textarea name="message" rows=5 cols=42></textarea>
            </td>
        </tr>
        <tr>
            <td colspan=2 style="text-align: center;"><input type="submit" value="Send Notification" /></td>
        </tr>
        </table>
    </form>
</body>
</html>


FCM PUSH PHP 서버 샘플 소스를 첨부한다.


FCM_Push_Sample.zip


728x90
블로그 이미지

Link2Me

,
728x90

last Update : 2019.9.2

사용자 기기의 고유 토큰 정보를 획득하는 방법이 변경되었다.

// 사용자 기기의 고유 토큰 정보를 획득
FirebaseInstanceId.getInstance().getInstanceId().addOnSuccessListener( Login.this,  new OnSuccessListener<InstanceIdResult>() {
    @Override
    public void onSuccess(InstanceIdResult instanceIdResult) {
        newToken = instanceIdResult.getToken();
        Log.e("newToken",newToken);

    }
});

Button submit = (Button) findViewById(R.id.login_btn);
submit.setOnClickListener(new Button.OnClickListener() {
    @Override
    public void onClick(View view) {
        loginID = etId.getText().toString().trim();
        loginPW = etPw.getText().toString().trim();

        if (loginID != null && !loginID.isEmpty() && loginPW != null && !loginPW.isEmpty()) {
            Uri.Builder builder = new Uri.Builder()
                    .appendQueryParameter("loginID", Value.encrypt(loginID))
                    .appendQueryParameter("loginPW", Value.encrypt(loginPW))
                    .appendQueryParameter("uID", Value.encrypt(Value.getPhoneUID(context)))
                    .appendQueryParameter("AppVersion", Value.VERSION)
                    .appendQueryParameter("phoneVersion", Build.VERSION.RELEASE)
                    .appendQueryParameter("phoneBrand", Build.BRAND)
                    .appendQueryParameter("phoneModel", Build.MODEL)
                    .appendQueryParameter("mfoneNo", Value.encrypt(getPhoneNumber()))
                    .appendQueryParameter("tokenID", newToken)
                    .appendQueryParameter("keyword", Value.encrypt(Value.URLkey()));
            String urlParameters = builder.build().getEncodedQuery();

            new AsyncLogin().execute(Value.IPADDRESS + "/loginChk.php", urlParameters);
        }
    }
});


========================================================================

Update : 2017.9.2


안드로이드 스튜디오에서 FCM 관련 코드를 구현했으면 이제 구글에서 생성해주는 토큰을 PHP 서버에 등록해야 한다.


토큰(Token)을 등록하는 방법은 로그인 정보에 같이 포함해서 보내는 것이 가장 편리하다.


// 단말기의 ID 정보를 얻기 위해서는 READ_PHONE_STATE 권한이 필요
TelephonyManager mTelephony = (TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE);
if (mTelephony.getDeviceId() != null){
    uID = mTelephony.getDeviceId();  // 스마트폰 기기 정보
} else {
    uID = Settings.Secure.getString(getApplicationContext().getContentResolver(), Settings.Secure.ANDROID_ID);
}

// 사용자 기기의 고유 토큰 정보를 획득
String getToken = FirebaseInstanceId.getInstance().getToken();

System.out.println("DeviceID : " + uID);
System.out.println("Intro Token : " + getToken);

// 전달할 인자들
String keyword = Value.URLkey();
Uri.Builder builder = new Uri.Builder()
        .appendQueryParameter("loginID", Value.encrypt(params[0]))
        .appendQueryParameter("loginPW", Value.encrypt(params[1]))
        .appendQueryParameter("uID", Value.encrypt(uID))
        .appendQueryParameter("phoneVersion", Build.VERSION.RELEASE)
        .appendQueryParameter("phoneBrand", Build.BRAND)
        .appendQueryParameter("phoneModel", Build.MODEL)
        .appendQueryParameter("tokenID", getToken)
        .appendQueryParameter("keyword", Value.encrypt(Value.URLkey()));
String urlParameters = builder.build().getEncodedQuery();
 


토큰에 대한 정보를 획득하여 로그인하면서 서버로 전송하면 된다.


이제 PHP 서버에서는 이 정보를 사용자 DB에 저장해야 한다.


loginChk.php 파일에서 안드로이드 접속 인증 체크를 한다고 가정하자.

loginID, loginPW 인증에 성공하면

// 토큰 등록 및 갱신
if(isset($tokenID) && strlen($tokenID) > 63){
    $c->registerToken($tokenID,$loginID,$deviceID); // DB에 토큰 저장
}


함수를 포함시켜서 DB에 토큰 정보가 저장되고 Update 되도록 한다.

아래 코드는 토큰 저장에 필요한 함수만 발췌했다.

<?php
class TokenClass {
    // 토큰 등록 및 갱신
    function registerToken($token,$userID,$deviceID){
        global $dbconn;
        $rs = $this->getDeviceChk($userID,$deviceID); // memberuid 또는 0
        if($rs > 0 ){
            $gettokenID = $this->getTokenIDChk($rs); // 0 또는 DB 등록 토큰 반환
            if(($gettokenID == '0') || ($gettokenID !== $token)){ // 등록안되었거나 토큰 불일치
                $sql ="UPDATE member_data SET tokenID='".$token."'";
                $sql.=" where memberuid='".$rs."'";
                if($result = mysqli_query($dbconn,$sql)){
                    return 1;
                } else {
                    return 0;
                }
            }
        }
    }

    function getTokenIDChk($rs){
        global $dbconn;
        $sql ="select tokenID from member_data where memberuid='".$rs."'";
        if($result = mysqli_query($dbconn,$sql)){
            $row = mysqli_fetch_row($result);
            if($row[0] == NULL){
                return 0;
            } else {
                return $row[0];
            }
        } else {
            return 0;
        }
    }

    function getDeviceChk($userID,$deviceID){
        global $dbconn;
        // 사용자 기준 장치 정보 검사
        $sql ="select count(phoneID),memberuid from member_data";
        $sql.=" where memberuid=(select uid from member_id where id='".$userID."') and phoneID='".$deviceID."'";
        if($result = mysqli_query($dbconn,$sql)){
            $row = mysqli_fetch_row($result);
            if($row[0] == 1){
                return $row[1]; // 있으면 memberuid 반환
            } else {
                return $row[0];
            }
        } else {
            return 0;
        }
    }

}//end class
?>
 



DB 테이블 구조

테이블 구조는 참조하여 필요한 것만 발췌하여 이용하면 된다.

member_id.uid = member_data.memberuid


CREATE TABLE IF NOT EXISTS `member_data` (
  `memberuid` int(11) NOT NULL,
  `site` int(11) NOT NULL DEFAULT '0',
  `auth` tinyint(4) NOT NULL DEFAULT '0',
  `level` int(11) NOT NULL DEFAULT '0',
  `admin` tinyint(4) NOT NULL DEFAULT '0',
  `email` varchar(50) NOT NULL DEFAULT '',
  `name` varchar(30) NOT NULL DEFAULT '',
  `nic` varchar(50) NOT NULL DEFAULT '',
  `grade` varchar(20) NOT NULL DEFAULT '',
  `photo` varchar(200) NOT NULL DEFAULT '',
  `sex` tinyint(4) NOT NULL DEFAULT '0',
  `officeNO` varchar(14) NOT NULL DEFAULT '',
  `mobileNO` varchar(14) NOT NULL DEFAULT '',
  `num_login` int(11) NOT NULL DEFAULT '0',
  `d_regis` varchar(14) NOT NULL DEFAULT '',
  `tokenID` varchar(200) DEFAULT NULL,
  `phoneID` varchar(60) DEFAULT NULL,
  `phoneVersion` varchar(20) DEFAULT NULL,
  `phoneBrand` varchar(20) DEFAULT NULL,
  `phoneModel` varchar(20) DEFAULT NULL,
  PRIMARY KEY (`memberuid`),
  KEY `site` (`site`),
  KEY `auth` (`auth`),
  KEY `level` (`level`),
  KEY `admin` (`admin`),
  KEY `email` (`email`),
  KEY `name` (`name`),
  KEY `nic` (`nic`),
  KEY `sex` (`sex`),
  KEY `d_regis` (`d_regis`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;


CREATE TABLE IF NOT EXISTS `member_id` (
  `uid` int(11) NOT NULL AUTO_INCREMENT,
  `site` int(11) NOT NULL DEFAULT '0',
  `id` varchar(50) NOT NULL DEFAULT '',
  `pw` varchar(50) NOT NULL DEFAULT '',
  `code` int(6) NOT NULL DEFAULT '0',
  `admin` int(2) NOT NULL DEFAULT '0',
  PRIMARY KEY (`uid`),
  UNIQUE KEY `id` (`id`),
  KEY `code` (`code`)
) ENGINE=MyISAM  DEFAULT CHARSET=utf8 AUTO_INCREMENT=1 ;


728x90
블로그 이미지

Link2Me

,
728x90

안드로이드에서 구현할 사항은 newToken 생성하여 DB에 저장되도록 하는 것, PUSH 메시지를 수신하면 팝업된 메시지 창을 누르면 해당 게시글로 이동하여 해당 게시글을 읽어서 내용을 확인하도록 하는 것이 주요 목적이다.


last Update : 2019.8.31

https://firebase.google.com/docs/cloud-messaging/android/client?authuser=0 를 참고하여 파일을 구현한다.


https://medium.com/android-school/firebaseinstanceidservice-is-deprecated-50651f17a148

FirebaseInstanceIdService is deprecated 내용이 나온다.

이걸 참조해서 newToken 생성하는 코드를 추가한다. (다음 게시글에 코드 추가했음)


AndroidManifest.xml 파일 수정사항

<service
    android:name=".FCMListenerService"
    android:stopWithTask="false">
    <intent-filter>
        <action android:name="com.google.firebase.MESSAGING_EVENT" />
    </intent-filter>
</service>
 


FCMListenerService.java 파일

public class FCMListenerService extends FirebaseMessagingService {
    private static final String TAG = "FCM";

    @Override
    public void onNewToken(String s) {
        super.onNewToken(s);
        Log.d(TAG, "token[" + s + "]" );
        /*
         * 기존의 FirebaseInstanceIdService에서 수행하던 토큰 생성, 갱신 등의 역할은 이제부터
         * FirebaseMessaging에 새롭게 추가된 위 메소드를 사용하면 된다.
         */
    }

    // [START receive_message]
    @Override
    public void onMessageReceived(RemoteMessage remoteMessage) {
        // 메시지를 받았을 때 동작하는 메소드
        String title = remoteMessage.getData().get("title");
        String message = remoteMessage.getData().get("message");
        Log.d(TAG, "From: " + remoteMessage.getFrom());
        Log.d(TAG, "Title: " + title);
        Log.d(TAG, "Message: " + message);

        NotificationHelper notificationHelper = new NotificationHelper(getApplicationContext());
        notificationHelper.createNotification(title,message);
    }
}


Android 8.0(API 수준 26) 이상부터는 알림 채널이 지원 및 권장된다. FCM은 기본적인 설정으로 기본 알림 채널을 제공한다.


NotificationHelper 는 https://link2me.tistory.com/1514 코드를 참조하면 된다.


===========================================================================

Update : 2017.9.2

FCM(Firebase Clouding Messaging) 을 서버(PHP)에서 PUSH 메시지를 보내면 안드로이드폰에서 받기 위해서는 관련 코드를 구현해야 한다.


GCM 에서 FCM 으로 Migrate 하는 방법에 대해서는 https://developers.google.com/cloud-messaging/android/android-migrate-fcm 에 자세히 설명되어 있다.


1. 토큰 등록 코드

파일명은 본인이 지정하고 싶은데로 지정하면 된다.

이 코드에 추가해서 member id 에 연계하여 토큰정보를 수집하도록 해도 되고, 로그인 코드에 추가해도 된다.


public class FCMInstanceIDListenerService extends FirebaseInstanceIdService {
private static final String TAG = "MyFirebaseIDService";

@Override
public void onTokenRefresh() {
    // Get updated InstanceID token.
    String token = FirebaseInstanceId.getInstance().getToken();
    Log.d(TAG, "Refreshed token: " + token);
}


2. FCM 메시지를 수신 받아서 처리하는 코드

    보통 메시지를 수신하면 메시지 제목만 팝업 메시지로 띄우고,

    클릭하면 메시지 내용을 볼 수 있는 UI로 이동하도록 코드를 구현한다.

 public class FCMListenerService extends com.google.firebase.messaging.FirebaseMessagingService {
    private static final String TAG = "FCM";

    int mLastId = 0;
    ArrayList<Integer> mActiveIdList = new ArrayList<Integer>();
    NotificationManager nm;

    // [START receive_message]
    @Override
    public void onMessageReceived(RemoteMessage remoteMessage) {
        // 메시지를 받았을 때 동작하는 메소드
        String title = remoteMessage.getData().get("title");
        String message = remoteMessage.getData().get("message");
        Log.d(TAG, "From: " + remoteMessage.getFrom());
        Log.d(TAG, "Title: " + title);
        Log.d(TAG, "Message: " + message);

        sendPushNotification(title,message);
    }

    private void createNotificationId() {
        int id = ++mLastId;
        mActiveIdList.add(id);
    }

    public void sendPushNotification(String title, String message) {
        nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
        nm.cancel(mLastId);
        createNotificationId();

        Uri defaultSoundUri= RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION);

        // 하위 호환성을 위해 NotificationCompat.Builder 사용

        NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(this)
                .setSmallIcon(R.mipmap.icon)
                .setContentTitle(title)
                .setSound(defaultSoundUri)
                .setLights(000000255,500,2000)
                .setAutoCancel(true)
                .setWhen(System.currentTimeMillis())
                .setContentText(message);

        Intent popupIntent = new Intent(getApplicationContext(), Popup_Noti.class);
        popupIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
        popupIntent.putExtra("msg", title);
        popupIntent.putExtra("LastId", mLastId);
        startActivity(popupIntent); // 메시지 팝업창을 바로 띄운다.

        PendingIntent resultPendingIntent =PendingIntent.getActivity(this, 0, popupIntent, PendingIntent.FLAG_UPDATE_CURRENT);
        mBuilder.setContentIntent(resultPendingIntent);

        nm.notify(mLastId, mBuilder.build());
    }
}


getActivity(Context, int, Intent, int) -> Activity를 시작하는 인텐트를 생성함
getBroadcast(Context, int, Intent, int) -> BroadcastReceiver를 시작하는 인텐트를 생성함
getService(Context, int, Intent, int)  -> Service를 시작하는 인텐트를 생성함


사용자가 알림 창의 알림 텍스트를 클릭했을 때 Activity를 시작하려면, setContentIntent()를 호출하여 PendingIntent를 추가한다.


참조 : https://developer.android.com/guide/topics/ui/notifiers/notifications?hl=ko



 public class Popup_Noti extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        requestWindowFeature(Window.FEATURE_NO_TITLE);
        // 키잠금 해제 및 화면 켜기
        getWindow().addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
                | WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD
                | WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON);

        AlertDialog.Builder builder = new AlertDialog.Builder(Popup_Noti.this);
        builder.setTitle(R.string.app_name);  // 앱의 이름
        builder.setMessage(getIntent().getStringExtra("msg")); // 넘겨받은 메시지 제목
        builder.setCancelable(false);
        builder.setPositiveButton("내용 보기", new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                Intent intent = new Intent(Popup_Noti.this, Notice.class); // 공지사항 정보 팝업 및 파싱처리
                startActivityForResult(intent, 0);
                NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
                nm.cancel(getIntent().getIntExtra("LastId", -1));
                finish();
            }
        });
        builder.setNegativeButton("닫기", new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                Intent intent = new Intent(Popup_Noti.this, Intro.class);
                intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP
                        | Intent.FLAG_ACTIVITY_SINGLE_TOP);
                startActivity(intent);
                finish();
            }
        });
        AlertDialog alertDialog = builder.create();
        alertDialog.show();
    }
}


3. AndroidManifest.xml 파일에 추가할 사항

 <service android:name="com.tistory.link2me.fcm.FCMListenerService">
    <intent-filter>
        <action android:name="com.google.firebase.MESSAGING_EVENT" />
    </intent-filter>
</service>
<service android:name="com.tistory.link2me.fcm.FCMInstanceIDListenerService">
    <intent-filter>
        <action android:name="com.google.firebase.INSTANCE_ID_EVENT" />
    </intent-filter>
</service>




728x90
블로그 이미지

Link2Me

,