2016년 8월 3일 수요일

Android Studio - 주소록, SMS, 음성인식 연동하여 간단 메시지 전송 앱 만들기


주요 체크포인트

- Permission 획득
- 외부 APP 연동
  . 주소록 정보 가져오기
  . 음성인식으로 텍스트 정보 채우기
  . SMS 전송결과 Broadcast 를 이용하여 Intent

package com.example.ch4_contacts_sms;

import android.Manifest;
import android.app.Activity;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.provider.ContactsContract;
import android.speech.RecognizerIntent;
import android.support.annotation.NonNull;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat;
import android.support.v7.app.AppCompatActivity;
import android.telephony.SmsManager;
import android.telephony.TelephonyManager;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Toast;

import java.util.ArrayList;
import java.util.List;

public class MainActivity extends AppCompatActivity implements View.OnClickListener {
    Button sendButton;
    Button voiceButton;
    Button contactButton;
    EditText phoneEdit;
    EditText contentEdit;

    boolean contactPermission;
    boolean smsPermission;
    boolean phonePermission;

    private final int REQUEST_FOR_CONTACT = 10;
    private final int REQUEST_FOR_VOICE = 20;
    private final int REQUEST_FOR_PERMISSION = 200;

    @Override    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        sendButton = (Button) findViewById(R.id.button_send);
        voiceButton = (Button) findViewById(R.id.button_voice);
        contactButton = (Button) findViewById(R.id.button_contacts);

        phoneEdit = (EditText) findViewById(R.id.edit_phone);
        contentEdit = (EditText) findViewById(R.id.edit_content);

        sendButton.setOnClickListener(this);
        voiceButton.setOnClickListener(this);
        contactButton.setOnClickListener(this);

        // api level 22까지는(5.1) manifest <uses-permission>으로 등록만 하면 실행되는 개발자 신고제        // 23(6.0) 부터는 개발자가 manifest에 아무리 등록했다고 하더라도 user가 환경설정에서 permission enable/disable 조절가능        // manifest에 등록했다고 끝이 아니라 코드에스 그 부분을 실행할 때 권한획득여부 체크 필수        contactPermission = smsPermission = phonePermission = false;
        if(ContextCompat.checkSelfPermission(this, Manifest.permission.READ_CONTACTS)== PackageManager.PERMISSION_GRANTED){
            contactPermission = true;
        }
        if(ContextCompat.checkSelfPermission(this, Manifest.permission.SEND_SMS)== PackageManager.PERMISSION_GRANTED){
            smsPermission = true;
        }
        if(ContextCompat.checkSelfPermission(this, Manifest.permission.READ_PHONE_STATE)== PackageManager.PERMISSION_GRANTED){
            phonePermission = true;
        }

        // 만약 disable로 되어있는 상태라면 runtime error 유발        // user에게 toast로 알려주고 끝내도 되지만, 강제로 permission을 조정할 수는 없으니        // permission 허가를 요청하도록하자        // user permission 허가를 하기 위해선 다시 환경설정으로 가기는 불편하니까        //  app에서 dialog permission 조정하도록 system dialog로 제공하자        if(!contactPermission && !smsPermission && !phonePermission){
            requestPermission();
        }
    }

    private void requestPermission(){
        // user에게 permission허용 dialog를 띄우는 역할        ActivityCompat.requestPermissions(this, new String[]{
                Manifest.permission.READ_CONTACTS,
                Manifest.permission.SEND_SMS,
                Manifest.permission.READ_PHONE_STATE        }, REQUEST_FOR_PERMISSION);
    }

    // requestPermission함수를 이용해서 유저에게 permission 조정 dialog를 띄웠다고 하더라도    // 여전히 유저가 거부했을 수도 있다.    // dialog를 띄웠다고 끝이 아니라 사후 추적해야 한다.    // requestPermission 함수에 의한 dialog 작업이 끝나는 순간 자동 호출    @Override    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        if(requestCode == REQUEST_FOR_PERMISSION && grantResults.length>0){
            if(grantResults[0] == PackageManager.PERMISSION_GRANTED){
                contactPermission = true;
            }
            if(grantResults[1] == PackageManager.PERMISSION_GRANTED){
                smsPermission = true;
            }
            if(grantResults[2] == PackageManager.PERMISSION_GRANTED){
                phonePermission = true;
            }
        }
    }

    public void onClick(View v) {
        if(v==contactButton){
            if(contactPermission){
                // 주소록의 목록화면을 띄운다.                Intent intent = new Intent(Intent.ACTION_PICK,
                        Uri.parse("content://com.android.contacts/data/phones"));
                // 결과를 돌려 받아야 한다.                startActivityForResult(intent, REQUEST_FOR_CONTACT);
            }else{
                requestPermission();
            }
        }else if(v == voiceButton){
            // 외부 app연동시 activity intent를 발생시키는데            //  intent에 의해 구동될 activity가 없을 수도 있다.            // intent를 발생시키기 전에 반응할 component가 있는지 체크 해야 함.
            // PackageManager: app이 설치된 static(run time시 동작중이지 않은)정보 추출            // 폰에 설치된 전체 app 목록(app package==app의구분자 획득 가능)            // intent에 반응할 component 정보
            // ActivityManager: PackageManger와 거의 쌍으로 등장하는 class            //                : 설치된 app runtime시에 살아서 움직이고 있는 정보            // 시스템에 동작중인 모든 process 목록 - pid 획득, 프로세스 kill            // 시스템에 동작중인 service 목록            // 폰의 top을 차지하고 있는 activity 정보            PackageManager pm = getPackageManager();
            List<ResolveInfo> activities = pm.queryIntentActivities(
                    // ACTION_RECOGNIZE_SPEECH action코드에 반응하는 activity get                    new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH), 0);
            if(activities.size() > 0){
                Intent intent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH);
                // free form: 말하는것 고대로 text                // web search: 말하는것 web search                intent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL,
                        RecognizerIntent.LANGUAGE_MODEL_FREE_FORM);
                // Dialog title 문자열                intent.putExtra(RecognizerIntent.EXTRA_PROMPT, "본문을 말하세요");
                // 원한다면 언어도 고정가능하고 지정하지 않으면 폰의 기본 로케일을 따름
                startActivityForResult(intent, REQUEST_FOR_VOICE);
           }else{
                Toast t = Toast.makeText(this, "음성인식을 지원하지 않는 폰", Toast.LENGTH_SHORT);
                t.show();
            }
        }else if(v == sendButton){
            if(smsPermission && phonePermission){
                // 유저 폰 번호를 sms 발신자 번호로 세팅                TelephonyManager telephonyManager = (TelephonyManager)getSystemService(TELEPHONY_SERVICE);
                String myNumber = telephonyManager.getLine1Number();

                // sent ack시 실행할 intent 의뢰 ==> PendingIntent                Intent intent = new Intent("SENT_SMS_ACTION");
                PendingIntent pendingIntent = PendingIntent.getBroadcast(getApplicationContext(),
                        0, intent, PendingIntent.FLAG_UPDATE_CURRENT);

                // sms 발송                SmsManager smsManager = SmsManager.getDefault();
                smsManager.sendTextMessage(
                        phoneEdit.getText().toString(),
                        myNumber,
                        contentEdit.getText().toString(),
                        pendingIntent,
                        null                );
            }else{
                requestPermission();
            }
        }
    }

    // startActivityForResult에 의한 요청이 되돌아 올때 자동 호출    // requestCode:  intent를 발생시킨 곳에서 intent를 구분하기 위해서 지정하는 임의의 숫자    //     ex) startActivityForResult(intent, 10); 에서 '10'    // resultCode: intent에 의해 수행된 곳에서 결과를 되돌리기전에 상태를 표현하기 위한 값    @Override    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        if(requestCode == REQUEST_FOR_CONTACT && resultCode == RESULT_OK) {
            // 주소록 목록에서 홍길동을 눌러 되돌아 왔다고 하더라도            // 홍길동의 전화번호가 넘어오지는 않는다.            // 홍길동을 식별하기 위한 식별자 값만 넘어온다.            // url 문자열로 url의 맨 마지막 단어가 식별자값이다.            String id = Uri.parse(data.getDataString()).getLastPathSegment();

            // id값을 조건으로 구체적으로 원하는 데이터를 요청 provider 이용            // select row의 집합객체            // cursor를 움직여서 row하나씩 선택하고 선택된 row column data 추출            Cursor cursor = getContentResolver().query(
                    // provider 식별자 uri                    ContactsContract.Data.CONTENT_URI,
                    // select column조건, 뽑고자 하는 데이터                    new String[]{ContactsContract.CommonDataKinds.Phone.NUMBER},
                    // select where조건                    ContactsContract.Data._ID + '=' + id,
                    // where쿼리문에 (?)이 있을경우에 들어갈 args                    null,
                    // orderby 문자열                    null            );

            // row 선택            cursor.moveToFirst();   // 어차피 해당 id를 갖는 1개의 연락처만 선택됨
            // data추출 및 화면 출력            phoneEdit.setText(cursor.getString(0));
        }else if(requestCode == REQUEST_FOR_VOICE && resultCode == RESULT_OK){
            ArrayList<String> results = data.getStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS);
            contentEdit.setText(results.get(0));
        }
    }

    // sms send후에 시스템에 넘어오는 sen ack시에 실행될 receiver    BroadcastReceiver sentReceiver =
            new BroadcastReceiver() {
                @Override                public void onReceive(Context _context, Intent _intent) {
                    String msg="";
                    switch (getResultCode()) {
                        case Activity.RESULT_OK:
                            // 전송 성공 처리; break;                            msg="sms 전송 성공";
                            break;
                        case SmsManager.RESULT_ERROR_GENERIC_FAILURE:
                            // 일반적인 실패 처리; break;                            msg="sms 전송 실패";
                            break;
                        case SmsManager.RESULT_ERROR_RADIO_OFF:
                            // 무선 꺼짐 처리; break;                            msg="무선 꺼짐";
                            break;
                        case SmsManager.RESULT_ERROR_NULL_PDU:
                            // PDU 실패 처리; break;                            msg="pdu 오류";
                            break;
                    }
                    Toast t = Toast.makeText(MainActivity.this, msg,
                            Toast.LENGTH_SHORT);
                    t.show();
                }
            };

    protected void onResume() {
        super.onResume();
        registerReceiver(sentReceiver, new IntentFilter("SENT_SMS_ACTION"));
    };
    @Override    protected void onPause() {
        // TODO Auto-generated method stub        super.onPause();
        unregisterReceiver(sentReceiver);
    }

}

2016년 8월 2일 화요일

Android 이론 - Service 사용 시 고려사항


서비스는 항상 Background 에서 동작

Background에서 지속적으로 NW 통신을 하게 된다는건 불합리

날씨를 가져다가 갱신하는 앱 위젯인데

화면이 Off 된 상태에서는 필요 없는 통신이므로

screen on receiver 에 반응해서 통신을 시작 하도록 설계하는것이 중요

동적으로 manifest에 등록 하거나 등록 해제 하도록 설계해야 함

Android Studio - Android IPC인 AIDL


RPC(Remote Procedure Call)
 : 서로다른 네트워크 또는 프로세스에 존재하는 Procedure(함수)를 호출하기 위한 기술

안드로이드에서는 AIDL(Android Interface Definition Language)로 제공 하고 있다.

IPC를 하는 Service와 Activity 간에 AIDL 이라는 인터페이스를 공유해서
Activity - Service 가 Bind Connect 된 이후에 Activity 에서 Service에서 구현 된 AIDL 인터페이스를 활용할 수 있도록 하는 기술이다.


1. AIDL 서비스 앱 생성
2. AIDL Client 앱 생성
 

[AIDL 서비스 앱]
1. res/raw 에 mp3 파일 준비
2. aidl 폴더, 패키지 및 aidl 파일 생성

IPlayService.aidl

// IPlayService.aidlpackage com.example.ch3_aidl;

// Declare any non-default types here with import statements
interface IPlayService {
    int currentPosition();
    int getMaxDuration();
    void start();
    void stop();
    int getMediaStatus();
}

3. Service 생성

PlayService.java

package com.example.ch3_aidl;

import android.app.Service;
import android.content.Intent;
import android.os.IBinder;

public class PlayService extends Service {
    public PlayService() {
    }

    @Override    public IBinder onBind(Intent intent) {
        // TODO: Return the communication channel to the service.        throw new UnsupportedOperationException("Not yet implemented");
    }
}

4. manifest 수정

자동 생성된 service 태그 아래

<service    android:name=".PlayService"
    android:enabled="true"
    android:exported="true">
    <intent-filter>
        <action android:name="com.multi.ACTION_PLAY"/>
    </intent-filter>
</service>

*5. 빌드 한번 하기

Build -> Make Module 'AIDL모듈명'
진행 해야 현 소스코드에서 해당 AIDL 인지 가능


6. 서비스 구현

package com.example.ch3_aidl;

public class CommonProperties {
   public static final int MEDIA_STATUS_STOP = 0;
   public static final int MEDIA_STATUS_RUNNING = 1;
   public static final int MEDIA_STATUS_COMPLETED = 2;
}


package com.example.ch3_aidl;

import android.app.Service;
import android.content.Intent;
import android.media.MediaPlayer;
import android.os.IBinder;
import android.os.RemoteException;

public class PlayService extends Service {

    MediaPlayer player;
    int status = 0;

    public PlayService() {
    }

    @Override    public void onCreate() {
        super.onCreate();
        player = new MediaPlayer();
    }

    @Override    public void onDestroy() {
        super.onDestroy();
        player.release();
    }

    @Override    public IBinder onBind(Intent intent) {
        // aidl 파일을 구현한 stub 객체(실 업무 객체가 넘어가는게 아니라)를 리턴        return new IPlayService.Stub(){// Make Module xxx 작업을 해서 에러가 발생하지 않음
            @Override            public int currentPosition() throws RemoteException {
                if(player.isPlaying()){
                    return player.getCurrentPosition();
                }else{
                    return 0;
                }
            }

            @Override            public int getMaxDuration() throws RemoteException {
                if(player.isPlaying()){
                    return player.getDuration();
                }else{
                    return 0;
                }
            }

            @Override            public void start() throws RemoteException {
                if(!player.isPlaying()){
                    player = MediaPlayer.create(PlayService.this, R.raw.music);
                    player.start();

                    player.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
                        @Override                        public void onCompletion(MediaPlayer mp) {
                            status = CommonProperties.MEDIA_STATUS_COMPLETED;
                        }
                    });
                }

                status = CommonProperties.MEDIA_STATUS_RUNNING;
            }

            @Override            public void stop() throws RemoteException {
                if(player.isPlaying())
                    player.stop();
                status = CommonProperties.MEDIA_STATUS_STOP;
            }

            @Override            public int getMediaStatus() throws RemoteException {
                return status;
            }
        };
    }
}




[AIDL Client 앱]

1. aidl 폴더, 패키지 생성 및 aidl 파일 복사

2. 빌드 한번 하기

Build -> Make Module 'AIDL모듈명'
진행 해야 현 소스코드에서 해당 AIDL 인지 가능

3. MainActivity

package com.example.ch3_aidl_client;

import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.os.RemoteException;
import android.os.SystemClock;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.View;
import android.widget.ImageButton;
import android.widget.ProgressBar;

import com.example.ch3_aidl.IPlayService;


public class MainActivity extends AppCompatActivity implements View.OnClickListener {

    IPlayService pService;
    ImageButton start;
    ImageButton stop;
    ProgressBar mProgress;
    boolean isRunning = true;
    ProgressThread pt;
    Handler handler;
    Intent intent;


    //--add 1---------------    // bindService callback함수를 가지는 interface구현 클래스    ServiceConnection connection = new ServiceConnection() {
        // bindService에 의해 bind객체가 넘어온 순간 호출        // 두번째 객체가 bind 객체 stub 객체가 넘어 옴        @Override        public void onServiceConnected(ComponentName name, IBinder service) {
            pService = IPlayService.Stub.asInterface(service);
            start.setEnabled(true);
            checkService();
        }

        @Override        public void onServiceDisconnected(ComponentName name) {
            pService = null;
        }
    };
   //------------------------
    @Override    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        start = (ImageButton) findViewById(R.id.start);
        stop = (ImageButton) findViewById(R.id.stop);
        mProgress = (ProgressBar) findViewById(R.id.pb);
        mProgress.setProgress(0);

        start.setOnClickListener(this);
        stop.setOnClickListener(this);
        start.setEnabled(false);
        stop.setEnabled(false);

        handler = new Handler() {
            // Activity ANR - Thread에서 sendMessage하는 순간 UI Thread에 의해 자동 호출            @Override            public void handleMessage(Message msg) {
                switch(msg.what) {
                    case 1:    // stopped media                        start.setEnabled(true);
                        stop.setEnabled(false);
                        mProgress.setProgress(0);
                        break;
                }
                super.handleMessage(msg);
            }
        };

      //add2-------------------------------------        // service 구동을 위한 intent 준비        intent = new Intent("com.multi.ACTION_PLAY");
        // 이전버전에서는 그냥 intent를 실행. lollipop부터는 bindService의 경우 package명을 명시        intent.setPackage("com.example.ch3_aidl");
      //--------------------------------------    }

    private void checkService() {
        if(pService != null) {
            try {
                if(pService.getMediaStatus() == CommonProperties.MEDIA_STATUS_STOP) {
                    Log.d("kkang", "MEDIA_STATUS_STOP");
                    stop.setEnabled(false);
                } else if(pService.getMediaStatus() == CommonProperties.MEDIA_STATUS_RUNNING) {
                    Log.d("kkang", "MEDIA_STATUS_RUNNING");
                    start.setEnabled(false);
                    isRunning = true;
                    pt = new ProgressThread();
                    pt.start();
                }
            } catch (RemoteException e) {

            }
        }

    }

    //add3-------------------------------
    @Override    protected void onResume() {
        super.onResume();
        bindService(intent, connection, Context.BIND_AUTO_CREATE);
        checkService();
    }

    @Override    protected void onPause() {
        super.onPause();
        unbindService(connection);
        isRunning = false;
    }
    //---------------------------------
    @Override    public void onClick(View v) {
        if(v == start) {
            try {
                pService.start();
                mProgress.setMax(pService.getMaxDuration());
                isRunning = true;
                pt = new ProgressThread();
                pt.start();

                start.setEnabled(false);
                stop.setEnabled(true);
            } catch (RemoteException e) {
                // TODO Auto-generated catch block                e.printStackTrace();
            }
        } else if(v == stop) {
            try {
                pService.stop();
                isRunning = false;
                start.setEnabled(true);
                stop.setEnabled(false);
            } catch (RemoteException e) {
                // TODO Auto-generated catch block                e.printStackTrace();
            }
        }

    }


    class ProgressThread extends Thread {
        @Override        public void run() {
            while(isRunning) {
                try {

                    if(pService.getMediaStatus() == CommonProperties.MEDIA_STATUS_COMPLETED) {
                        handler.sendEmptyMessage(1);
                        break;
                    } else {
                        mProgress.setProgress(pService.currentPosition());
                        SystemClock.sleep(1000);
                    }
                } catch (RemoteException e) {
                    // TODO Auto-generated catch block                    e.printStackTrace();
                }
            }
        }
    }
}

Android 이론 - Time out 문제 (5초, 10초 룰)


안드로이드에는 2가지 TIMEOUT 제약이 있다.

5초룰과 10초룰

5초룰로 더 유명한 에러는 ANR이라고 일컬어 지는 에러이다.
ANR 이란 Activity Not Response 의 약자 이다.

사전적 의미는 Activity 응답 없음

개발 하면서 고려 해야 할 것은 화면 event 발생으부터 logic 처리 완료까지 5초 안에 끝내야 한다는 것이다.
대부분의 네트워킹 관련 로직이 있으면 내포하고 있는 문제이다.
안드로이드 개발자라면 반드시 고려하고 구현해야 하는 부분이다.

[솔루션]
1. Thread로 처리 하면 문제 없음
 -> android 개발자라면 NW 처리 이벤트는 무조건 Thread 처리

BUT, Thread로 처리하면 생기는 문제

사용자 정의 Thread 는 UI Thread 에서 생성할텐데
문제는 개발자 정의 Thread 가 View 객체에 접근이 불가하다는 것이다.(Run time error)
View 객체는 UI Thread 에서만 제어 가능하다.
 -> Handler 로 해결 가능, Thread 에서 Handler 를 이용하면 의뢰 형식으로 간접제어 가능

2. IntentService

IntentService 는 서비스와 거의 동일하지만,
해당 작업이 완료 되면 알아서 종료 (Service 는 스스로 종료 불가)
완료된 작업은 ResultReceiver 이용


p.s.
10초룰
 : Broadcast receiver 가 logic 처리 완료까지의 시간 10초


Android Studio - Tab 화면을 ViewPager 와 연동 하자


1. main_activity.xml 에 TabLayout 추가
2. MainActivity.java 에 TabLayout 변수를 선언하여 컨트롤
3. ViewPager와 연결


[main_activity.xml 에 TabLayout 추가]

<Toolbar> 태그 밑에다가 추가

<android.support.design.widget.TabLayout

    android:id="@+id/tabs"
    android:layout_width="match_parent"
    android:layout_height="50dp"
    android:layout_below="@id/toolbar"
    android:layout_gravity="bottom"
    app:layout_collapseMode="parallax"
    app:tabGravity="center"
    app:tabMode="scrollable">
</android.support.design.widget.TabLayout>


[MainActivity.java 에 TabLayout 변수를 선언하여 컨트롤]

1. TabLayout 의 이벤트 처리를 해당 클래스에서 처리

tabLayout 의 이벤트 처리를 MainActivity 에서 하도록, eventlistner를 implement 하고

public class MainActivity extends AppCompatActivity        
      implements TabLayout.OnTabSelectedListener

추상함수 구현
Tab을 선택할 경우 ViewPager를 이동 시키는 것은 개발자가 수동으로 해줘야 함.

@Overridepublic void onTabSelected(TabLayout.Tab tab) {
    viewPager.setCurrentItem(tab.getPosition());
}

@Overridepublic void onTabUnselected(TabLayout.Tab tab) {

}

@Overridepublic void onTabReselected(TabLayout.Tab tab) {

}


2. onCreate 함수 안에 아래 코드를 추가

// TabLayout
TabLayout tabLayout = (TabLayout)findViewById(R.id.tabs);
// 아래의 코드로 유저가 viewPager화면 조정시 tab도 자동조정
// 반대로는 개발자가 이벤트 처리로
// tab button의 문자열은 이곳에서 직접줄수도 있고
// 아래 코드에 의해 viewPager의 문자열을 그대로 적용할 수도 있다.
tabLayout.setupWithViewPager(viewPager);
tabLayout.addOnTabSelectedListener(this);


[ViewPager와 연결]

MyPagerAdapter 내부클래스에 Tab의 title 정보를 추가 및 getter 추가

class MyPagerAdapter extends FragmentStatePagerAdapter {

    ....
    // viewPager를 위한 PagerTitleStrip, PagerTabStrip에 나올 문자열    // 우리의 경우에는 TabLayout에서 이용한다.    String titles[] = new String[]{"TAB1", "TAB2", "TAB3"};

    @Override    public CharSequence getPageTitle(int position) {
        return titles[position];
    }
    
    ....

}