message app 열기

 

URL형태 : sms:전화번호&body=메시지

 

 

ViewController.swift

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import UIKit
 
class ViewController: UIViewController {
    
    @IBOutlet var editMessage: UITextField!
 
    
    override func viewDidLoad() {
        super.viewDidLoad()
 
    }
 
    // 메시지앱으로 이동
    @IBAction func moveToMessageApp(_ sender: UIButton) {
        let message = editMessage.text
        var sms : String = "sms:01012341234"
 
        // 메시지 내용이 있으면 전화번호 뒤에 &body=메시지를 붙이고 인코딩을 한다
        if let msg = message , !msg.isEmpty {
            sms = sms + "&body=" + msg
        }
        sms = sms.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)!   // 인코딩
 
        UIApplication.shared.open( URL.init(string: sms)!, options: [:], completionHandler: nil )
    }
}
 
 

 

 

결과

버튼 클릭시 메시지앱으로 이동하여 전화번호와 메시지를 설정한다

PickerView 예제

 

텍스트 필드 입력시 피커뷰 띄우기

피커뷰 위에 툴바를 추가하여 확인/취소 버튼 구현

 

 

ViewController.swift

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
import UIKit
 
class ViewController: UIViewController , UIPickerViewDelegate , UIPickerViewDataSource {
    
    @IBOutlet var editCity: UITextField!
    let pickerView = UIPickerView()
    var selectCity = "" // 피커뷰 선택시 값을 갖고 있다가 확인버튼 클릭시 텍스트필드에 세팅한다
    
    
    let pickerData = ["서울" , "경기도" , "인천" , "부산" , "대구" , "광주" , "대전" , "제주도"]  // 피커뷰에 보여줄 테스트 데이터
    
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        pickerView.frame = CGRect(x: 0, y: 0, width: self.view.frame.width, height: 220)
        pickerView.delegate = self
        pickerView.dataSource = self
        
        // 피커뷰 툴바추가
        let pickerToolbar : UIToolbar = UIToolbar()
        pickerToolbar.barStyle = .default
        pickerToolbar.isTranslucent = true  // 툴바가 반투명인지 여부 (true-반투명, false-투명)
        pickerToolbar.backgroundColor = .lightGray
        pickerToolbar.sizeToFit()   // 서브뷰만큼 툴바 크기를 맞춤
        // 피커뷰 툴바에 확인/취소 버튼추가
        let btnDone = UIBarButtonItem(title: "확인", style: .done, target: self, action: #selector(onPickDone))
        let space = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: self, action: nil)
        let btnCancel = UIBarButtonItem(title: "취소", style: .done, target: self, action: #selector(onPickCancel))
        pickerToolbar.setItems([btnCancel , space , btnDone], animated: true)   // 버튼추가
        pickerToolbar.isUserInteractionEnabled = true   // 사용자 클릭 이벤트 전달
        
        editCity.inputView = pickerView // 피커뷰 추가
        editCity.inputAccessoryView = pickerToolbar // 피커뷰 툴바 추가
        
    }
    
    // 피커뷰 > 확인 클릭
    @objc func onPickDone() {
        editCity.text = selectCity
        editCity.resignFirstResponder()
        selectCity = ""
    }
    
    // 피커뷰 > 취소 클릭
    @objc func onPickCancel() {
        editCity.resignFirstResponder() // 피커뷰를 내림 (텍스트필드가 responder 상태를 읽음)
        selectCity = ""
    }
 
    
    // 피커뷰의 구성요소(컬럼) 수
    func numberOfComponents(in pickerView: UIPickerView) -> Int {
        return 1    // 구성요소(컬럼)로 지역만 있으므로 1을 리턴
    }
    
    // 구성요소(컬럼)의 행수
    func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int-> Int {
        return pickerData.count
    }
 
    // 피커뷰에 보여줄 값 전달
    func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int-> String? {
        return pickerData[row]
    }
    
    // 피커뷰에서 선택시 호출
    func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
        selectCity = pickerData[row]
    }
}
 
 

 

 

결과

텍스트 필드 클릭시 피커뷰가 나타나고 피커뷰에서 선택 후 확인 버튼 클릭시 텍스트 필드에 값이 세팅된다

 

 

WebView를 스크롤하면 툴바 숨기기

 

CoordinatorLayout 과 NestedScrollView 사용

 

 

build.gradle (:app)

1
2
3
    implementation 'androidx.appcompat:appcompat:1.1.0'
 
 

- Material Design 라이브러리를 추가

 

 

styles.xml

1
2
3
4
5
6
7
8
9
10
11
12
<resources>
 
    <!-- Base application theme. -->
    <style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
        <!-- Customize your theme here. -->
        <item name="colorPrimary">@color/colorPrimary</item>
        <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
        <item name="colorAccent">@color/colorAccent</item>
    </style>
 
</resources>
 
 

- 툴바를 레이아웃 xml에 추가할 것이므로 styles에서는 액션바가 없는 스타일을 상속한다

 

 

activity_main.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
<?xml version="1.0" encoding="utf-8"?>
<!--
CoordinatorLayout : 부모뷰-자식뷰 , 자식뷰들간의 여러 인터렉션을 지원하는 컨테이너
NestedScrollView : 한 화면에 여러개의 스크롤 사용
 
fitsSystemWindows : status bar와 ui가 안겹치도록 조정 (뷰가 상태바와 소프트키 영역을 제외한 영역까지만 차지한다)
-->
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true"
    tools:context=".MainActivity">
 
 
        android:fitsSystemWindows="true"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">
 
        <!--
        layout_scrollFlags : AppBarLayout 및 하위의 스크롤 동작 담당
            scroll :  내용과 같이 스크롤, 툴바를 다시 볼려면 스크롤을 위로 끝까지 올려야함
            enterAlways : 맨위에 없어도 위로 스크롤시 바로 툴바가 보임
            enterAlwaysCollapsed : enterAlways와 동작이 비슷하지만 스크롤을 위로 끝까지 올려야 전체뷰가 보임 (스크롤하면 툴바도 같이 스크롤되어 안보임)
            exitUntilCollapsed : 아래/위로 스크롤시 축소된 툴바가 보이고 스크롤을 위로 끝까지 올려야 전체뷰가 보임 (스크롤해도 축소된 툴바가 보임)
            snap : 툴바가 위쪽에서 얼마나 떨어져 있는지에 따라 숨겨지거나 보여짐
         -->
            android:layout_width="match_parent"
            android:layout_height="?android:attr/actionBarSize"
            app:layout_scrollFlags="scroll|enterAlways"
            app:contentInsetStart="0dp"
            app:contentInsetEnd="0dp"
            app:contentInsetLeft="0dp"
            app:contentInsetRight="0dp"
            app:contentInsetStartWithNavigation="0dp">
 
            <LinearLayout
                android:paddingLeft="8dp"
                android:layout_width="match_parent"
                android:layout_height="match_parent">
                <TextView
                    android:text="툴바"
                    android:textSize="16dp"
                    android:textColor="#ffffff"
                    android:layout_gravity="center_vertical"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content" />
            </LinearLayout>
 
 
 
 
    <!-- layout_behavior : 자식뷰의 변화 상태를 부모뷰/다른 자식뷰한테 전달 (여기서는 스크롤했을 경우 다른 자식뷰에게 전달한다) -->
        android:layout_width="match_parent"
        android:layout_height="match_parent">
 
        <LinearLayout
            android:orientation="vertical"
            android:layout_width="match_parent"
            android:layout_height="match_parent">
 
            <WebView
                android:id="@+id/webView"
                android:layout_width="match_parent"
                android:layout_height="wrap_content" />
 
        </LinearLayout>
 
 
 
 
 

- 아래로 스크롤하면 툴바가 사라졌다가 위로 스크롤하면 툴바가 나타난다

 

 

MainActivity.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
 
 
public class MainActivity extends AppCompatActivity {
 
    // ui
    WebView webView;
 
 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
 
        webView = (WebView)findViewById( R.id.webView );
        webView.setNetworkAvailable( true );
 
        WebSettings settings = webView.getSettings();
        settings.setJavaScriptEnabled( true );
 
        webView.setWebChromeClient( new WebChromeClient() );
        webView.setWebViewClient( new WebViewClient() );
 
        webView.loadUrl( "https://www.naver.com");
    }
}
 
 

- 테스트를 위해 웹뷰에 네이버를 띄운다

 

 

결과

처음에는 툴바가 보였다가 아래로 스크롤하면 툴바가 사라졌다가 위로 조금만 스크롤해도 툴바가 다시 보인다

Glide 라이브러리로 gif이미지 로딩 

 

github : https://github.com/bumptech/glide

 

 

build.gradle (:app)

1
2
3
4
5
    // Glide
    // annotationProcessor : annotation processor classpath 에서 compile processor classpath를 분리하여 빌드 성능개선
    annotationProcessor 'com.github.bumptech.glide:compiler:4.11.0'
 
  • glide 라이브러리 추가

 

 

MainActivity.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
 
 
 
public class MainActivity extends AppCompatActivity {
 
    ImageView imgGif;
 
 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
 
        imgGif = (ImageView)findViewById( R.id.img_gif );
 
        Glide.with( this )
                .asGif()    // GIF 로딩
                .load( R.raw.loading )
                .diskCacheStrategy( DiskCacheStrategy.RESOURCE )    // Glide에서 캐싱한 리소스와 로드할 리소스가 같을때 캐싱된 리소스 사용
                .into( imgGif );
    }
}
 
 
  • GIF 이미지를 raw 폴더에 넣음

 

그외 glide 라이브러리 함수

  • override() : 지정한 크기로 이미지 사이즈 설정

  • placeholder() : 이미지가 로딩할 동안 보여줄 기본 이미지

  • error() : 이미지 로딩 실패시 보여줄 에러 이미지

 

 

activity_main.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout 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:padding="8dp"
    tools:context=".MainActivity">
 
    <ImageView
        android:id="@+id/img_gif"
        android:layout_centerInParent="true"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />
 
</RelativeLayout>
 
 

 

 

결과

Retrofit2 예제

안전한 타입의 HTTP Client 라이브러리로 Android 및 Java 애플리케이션에서 사용합니다

최소요구사항 : Java 8+ or Android API 21+

 

github : https://github.com/square/retrofit

 

 

build.gradle (:app)

1
2
3
4
    // retrofit2
    implementation group: 'com.squareup.retrofit2', name: 'retrofit', version: '2.8.1'
    implementation group: 'com.squareup.retrofit2', name: 'converter-gson', version: '2.8.1' // JSON을 직렬화
    implementation group: 'com.google.code.gson', name: 'gson', version: '2.8.6' // 직렬화된 JSON을 객체로 역직렬화
  • Retrofit2 라이브러리 추가

 

 

AndroidManifest.xml

1
<uses-permission android:name="android.permission.INTERNET" />
  • 인터넷 사용 권한 추가

 

 

ApiInterface.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
 
public interface ApiInterface {
 
    // base_url + "api/login" 으로 POST 통신
    @POST("api/login")
    Call<ResLoginData> requestPostLogin(@Body ReqLoginData reqLoginData );   // @Body : request 파라미터
 
    // base_url + "api/users" 으로 GET 통신
    @GET("api/users")
    Call<ResUsersData> requestGetUsersDetail( @Query(value = "page", encoded = trueString page );   // @Query : url에 쿼리 파라미터 추가, encoded - true
 
}
http://colorscripter.com/info#e" target="_blank" style="color:#e5e5e5text-decoration:none">Colored by Color Scripter
  • HTTP 통신 인터페이스

 

 

HttpClient.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import retrofit2.Retrofit;
 
public class HttpClient {
 
    private static Retrofit retrofit;
 
    // Http 통신을 위한 Retrofit 객체반환
    public static Retrofit getRetrofit() {
        if( retrofit == null )
        {
            Retrofit.Builder builder = new Retrofit.Builder();
            builder.baseUrl( "https://reqres.in/" );
            builder.addConverterFactory( GsonConverterFactory.create() );  // 받아오는 Json 구조의 데이터를 객체 형태로 변환
 
            retrofit = builder.build();
        }
 
        return retrofit;
    }
}
 
http://colorscripter.com/info#e" target="_blank" style="color:#e5e5e5text-decoration:none">Colored by Color Scripter
  • Retrofit 객체를 생성

 

 

ReqLoginData.java

1
2
3
4
5
6
7
8
9
10
11
// api/login 요청 데이터
public class ReqLoginData {
 
    String email;
    String password;
 
    public ReqLoginData( String email, String password ) {
        this.email = email;
        this.password = password;
    }
}
http://colorscripter.com/info#e" target="_blank" style="color:#e5e5e5text-decoration:none">Colored by Color Scripter
  • api/login 통신의 요청 객체

 

 

ResLoginData.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
 
import androidx.annotation.NonNull;
 
// api/login 응답 데이터
public class ResLoginData {
 
    @Expose
    String token;
 
    @NonNull
    @Override
    public String toString() {
        return "[ResLoginData] token=" + token;
    }
}
http://colorscripter.com/info#e" target="_blank" style="color:#e5e5e5text-decoration:none">Colored by Color Scripter
  • api/login 통신의 응답 객체

  • api/login 통신 결과값을 ResLoginData 객체에 맵핑한다

 

 

ResUsersData.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
 
 
import androidx.annotation.NonNull;
 
// api/users 응답 데이터
public class ResUsersData {
 
    @Expose
    int page;
    @Expose
    @SerializedName("per_page")
    int perPage;
    @Expose
    int total;
    @Expose
    @SerializedName("total_pages")
    int totalPages;
    @Expose
    List<Data> data;
    @Expose
    Ad ad;
 
 
    public class Data {
        int id;
        String email;
        @SerializedName("first_name")
        String firstName;
        @SerializedName("last_name")
        String lastName;
        String avatar;
 
        @NonNull
        @Override
        public String toString() {
            return "{id=" + id + " , email=" + email + " , firstName=" + firstName + " , lastName=" + lastName + " , avatar}";
        }
    }
 
    public class Ad {
        String company;
        String url;
        String text;
 
        @NonNull
        @Override
        public String toString() {
            return "{company=" + company + " , url=" + url + " , text=" + text + "}";
        }
    }
 
    @Override
    public String toString() {
        String str = "[ResUsersData] page=" + page + " , perPage=" + perPage + " , total=" + total + " , totalPages=" + totalPages + " , data= [";
        forint i=0; i<data.size(); i++ ) {
            str += data.get(i).toString();
            if( i < data.size()-1 ) {
                str += ",";
            }
        }
        str += "]";
        str += " , ad=" + ad.toString();
 
        return str;
    }
}
http://colorscripter.com/info#e" target="_blank" style="color:#e5e5e5text-decoration:none">Colored by Color Scripter
  • api/users 통신의 응답 객체

  • api/users 통신 결과값을 ResUsersData 객체에 맵핑한다

 

 

MainActivity.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
 
import retrofit2.Callback;
import retrofit2.Response;
 
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
 
    private static String TAG = "MainActivity";
 
    ApiInterface api;
 
    // ui
    Button btnGet;
    Button btnPost;
    TextView txtResult;
 
 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
 
 
        btnGet = (Button)findViewById( R.id.btnGet );
        btnGet.setOnClickListener( this );
        btnPost = (Button)findViewById( R.id.btnPost );
        btnPost.setOnClickListener( this );
        txtResult = (TextView)findViewById( R.id.txtResult );
 
        api = HttpClient.getRetrofit().create( ApiInterface.class );
    }
 
    @Override
    public void onClick(View view) {
        switch ( view.getId() ) {
            case R.id.btnPost:
                txtResult.setText("");
                requestPost();
                break;
 
            case R.id.btnGet:
                txtResult.setText("");
                requestGet();
                break;
        }
    }
 
    // POST 통신요청
    public void requestPost() {
        ReqLoginData reqLoginData = new ReqLoginData( "eve.holt@reqres.in" , "cityslicka" );
        Call<ResLoginData> call = api.requestPostLogin( reqLoginData );
 
        // 비동기로 백그라운드 쓰레드로 동작
        call.enqueue( new Callback<ResLoginData>() {
            // 통신성공 후 텍스트뷰에 결과값 출력
            @Override
            public void onResponse(Call<ResLoginData> call, Response<ResLoginData> response) {
                txtResult.setText( response.body().toString() );    // body() - API 결과값을 객체에 맵핑
            }
 
            @Override
            public void onFailure(Call<ResLoginData> call, Throwable t) {
                txtResult.setText( "onFailure" );
            }
        } );
    }
 
    // GET 통신요청
    public void requestGet() {
        Call<ResUsersData> call = api.requestGetUsersDetail( "2" );
 
        // 비동기로 백그라운드 쓰레드로 동작
        call.enqueue(new Callback<ResUsersData>() {
            // 통신성공 후 텍스트뷰에 결과값 출력
            @Override
            public void onResponse(Call<ResUsersData> call, Response<ResUsersData> response) {
                txtResult.setText( response.body().toString() );
            }
 
            // 통신실패
            @Override
            public void onFailure(Call<ResUsersData> call, Throwable t) {
                txtResult.setText( "onFailure" );
            }
        });
    }
}
http://colorscripter.com/info#e" target="_blank" style="color:#e5e5e5text-decoration:none">Colored by Color Scripter
  • POST통신과 GET통신 결과값을 TextView에 출력

 

 

activity_main.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<?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:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >
 
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">
 
        <Button
            android:id="@+id/btnPost"
            android:text="POST"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" />
 
        <Button
            android:id="@+id/btnGet"
            android:text="GET"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" />
 
    </LinearLayout>
 
    <TextView
        android:id="@+id/txtResult"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />
 
 
</LinearLayout>
http://colorscripter.com/info#e" target="_blank" style="color:#e5e5e5text-decoration:none">Colored by Color Scripter

 

 

결과

바코드/QR코드 스캐너 개인정보처리방침

 


1. 바코드/QR코드 스캐너 앱은 어떠한 개인정보를 수집하거나 필요로 하지 않습니다.

 

2. 바코드/QR코드 스캐너 앱은 다음과 같은 권한을 필요로 합니다.

Camera

바코드/QR 코드를 스캔하기 위해 카메라 권한을 필요로 합니다.

  

Internet

광고를 수신하기 위해 인터넷 권한을 필요로 합니다.

 

Vibrate

바코드/QR 코드 스캔 성공시 진동을 주기 진동 권한을 필요로 합니다.

 

 

Bar code/QR code Personal information processing policy

 

1. Barcode / QR Code Scanner app does not collect or need any personal information.

2. The barcode / QR code scanner app requires the following privileges.

Camera

You need camera privileges to scan bar code / QR codes.


Internet

You need internet access to receive ads.


Vibrate

Bar code / QR code Vibration is required when scan is successful.

ADB Path 환경변수로 설정

 

 

1. ADB의 경로를 확인합니다

* [Android 설치경로]/sdk/platform-tools

 

 

2. VI로 .bash_profile 파일을 열고 수정합니다

1
ghj:~ ghj$ vi ~/.bash_profile
cs


1
2
export ADB_HOME=/Users/ghj/Library/Android/sdk/platform-tools
export PATH=${ADB_HOME}:$PATH
cs

* ADB_HOME 변수에 ADB 경로를 설정하고 PATH에 추가합니다

 

 

3. 터미널을 재실행하고 ADB 명령어로 확인합니다

1
2
3
4
ghj:~ ghj$ adb --version
Android Debug Bridge version 1.0.40
Version 4797878
Installed as /Users/ghj/Library/Android/sdk/platform-tools/adb
cs





Mac dnsmasq를 이용하여 DNS 서버 구축하기

* 호스트(Mac PC)와 내부망을 연결하고 와이파이를 공유하여 게이트웨이 역할을 하도록 합니다

호스트(Mac PC)에 dnsmasq 를 설치하여 DNS 서버를 구축합니다

* 그러면 단말기는 호스트(Mac PC)에 와이파이로 붙어서 도메인을 사용하여 내부망과 통신할 수 있습니다



1. Homebrew 설치하기

다음 명령어를 입력합니다

/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"

* 사이트 : https://brew.sh/index_ko



2. dnsmasq 설치하기

다음 명령어를 입력합니다

brew install dnsmasq

 

 

3. dnsmasq 설정하기

설정 디렉토리를 만듭니다

mkdir -pv $(brew --prefix)/etc/

* 디렉토리 경로를 알고 싶으면 다음 명령어를 입력합니다

  echo $(brew --prefix)/etc/ 


*.dev

$(brew --prefix)/etc/dnsmasq.conf 를 vi 에디터로 열어서 'address=/.dev/127.0.0.1' 를 추가합니다

 

자동

sudo cp -v $(brew --prefix dnsmasq)/homebrew.mxcl.dnsmasq.plist /Library/LaunchDaemons/

sudo launchctl load -w /Library/LaunchDaemons/homebrew.mxcl.dnsmasq.plist


nameserver를 resolver에 추가

sudo mkdir -v /etc/resolver

/etc/resolver/dev 를 vi 에디터로 열어서 'nameserver 127.0.0.1' 를 추가합니다

 

dnsmasq service 재시작

sudo launchctl stop homebrew.mxcl.dnsmasq

sudo launchctl start homebrew.mxcl.dnsmasq

 

 

4. Mac의 인터넷 공유를 시작합니다.

인터넷 공유 링크 : 



5. /etc/hosts 파일에 도메인과 ip주소를 입력하고 dnsmasq 서비스를 재시작합니다

1
2
3
4
5
6
7
8
9
10
11
12
##
# Host Database
#
# localhost is used to configure the loopback interface
# when the system is booting.  Do not change this entry.
##
127.0.0.1    localhost
255.255.255.255    broadcasthost
::1             localhost
 
 
192.168.2.1 lottodr.com
cs
 
 

6. 다른 디바이스MAC에 Wi-Fi로 붙은 후 도메인을 이용하여 접속합니다


GCM 구현2 - 서버

 

 

안드로이드 : http://ghj1001020.tistory.com/783?category=719597

서버 : http://ghj1001020.tistory.com/784?category=756570

 

 

가이드 : https://developers.google.com/cloud-messaging/http

 

1. 앱은 GCM 토큰을 가져와서 앱에 저장합니다

2. 서버에서 앱에 저장된 GCM 토큰을 사용하여 메시지를 요청합니다

3. 앱이 GCM 메시지 수신하고 알림을 띄웁니다

* 테스트 용도 이므로 토큰을 서버에 저장하는 부분은 제외했습니다. (서버에서는 디바이스의 GCM 토큰을 하드코딩해서 보냅니다)

 

 

1. gcm-server 라이브러리 이용

 

pom.xml

1
2
3
4
5
6
7
        <!-- GCM -->
        <!-- https://mvnrepository.com/artifact/com.google.gcm/gcm-server -->
        <dependency>
            <groupId>com.google.gcm</groupId>
            <artifactId>gcm-server</artifactId>
            <version>1.0.0</version>
        </dependency>
cs

* 라이브러리 jar 파일을 받아서 사용하실때는 json-simple 라이브러리도 필요합니다

* https://github.com/google/gcm/tree/master/client-libraries/java/rest-client/src/com/google/android/gcm/server

 

 

GcmSender.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
    //GCM Url
    private final String GCM_URL = "https://gcm-http.googleapis.com/gcm/send";
 
    ...
 
    //GCM 전송 - 라이브러리 사용
    public void gcmSend(Map<StringString> datas) {
        Message.Builder builder = new Message.Builder();
        builder.delayWhileIdle(true);    //기기가 절전 상태에서도 수신될지 여부
        builder.timeToLive(60*1000);    //기기가 비활성화 상태일때 메시지 유효시간 (초)
        builder.collapseKey(UUID.randomUUID().toString());    //중복메시지도 전송될 수 있도록 고유키 생성
        //데이터 추가
        builder.addData( "title", datas.get("title") );
        builder.addData( "body", datas.get("body") );
 
        //메시지 생성
        Message message = builder.build();
        
        //단말 토큰 리스트 
        ArrayList<String> registIdList = new ArrayList<>();
        registIdList.add(REG_ID);
                
        try {
            Sender sender = new Sender(SERVER_KEY);
            MulticastResult multiResult = sender.send(message, registIdList, 1);    //retry 1회
            List<Result> resultList = multiResult.getResults();
            //결과출력 
            for( Result result : resultList ) {
                if( result.getMessageId() != null ) {
                    //성공
                    System.out.println("Success");
                }
                else {
                    //실패
                    System.out.println("Fail : "+result.getErrorCodeName());
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
cs

* SERVER_KEY(서버키) 와 REG_ID(토큰) 는 각자 테스트 하시는 걸로 사용합니다

 

 

2. HttpsURLConnection 사용

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
    //GCM 전송 - HttpsUrlConnection 사용
    public void gcmSendUrl(Map<StringString> datas) {
        try {
            URL url = new URL(GCM_URL);
            HttpsURLConnection conn = (HttpsURLConnection) url.openConnection();
            conn.setRequestProperty("Authorization""key="+SERVER_KEY);
            conn.setRequestProperty("Content-Type""application/json; UTF-8");
            
            conn.setRequestMethod("POST");
            conn.setDoOutput(true);
            conn.setDoInput(true);
            
            JSONObject jsonMsg = new JSONObject();
            jsonMsg.put( "title", datas.get("title") );
            jsonMsg.put( "body", datas.get("body") );
            
            JSONObject json = new JSONObject();
            json.put("to", REG_ID);
            json.put("delay_while_idle"true);
            json.put("data", jsonMsg);
 
            System.out.println("Param : "+json.toString());
            
            conn.setRequestProperty("Content-Length"String.valueOf(json.toString().length()));
            
            //보내기
            OutputStreamWriter osw = new OutputStreamWriter(conn.getOutputStream(), "UTF-8");
            osw.write(json.toString());
            osw.flush();
            osw.close();
 
            //받기
            BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream(), "UTF-8"));
             
            StringBuffer strBuf = new StringBuffer();
            String inputLine;
            while((inputLine = in.readLine()) != null) { // response 출력
                strBuf.append(inputLine);
            }
            
            in.close();
            conn.disconnect();    
            
            //결과
            if(!strBuf.toString().isEmpty()) {
                System.out.println("Result : "+strBuf.toString());
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
cs

 

 

main

1
2
3
4
5
6
7
8
9
10
    public static void main(String[] args) {
        GcmSender gcmSender=  new GcmSender();
        //데이터
        Map<StringString> datas = new HashMap<>();
        datas.put("title""GCM 테스트");
        datas.put("body""GCM 테스트 중입니다.");
        
        gcmSender.gcmSend(datas);
//        gcmSender.gcmSendUrl(datas);
    }
cs

* title과 body 처럼 어떤 키-값으로 보낼지는 클라이언트와 미리 약속이 되어 있어야 합니다

 

 

결과

* 서버에서 전송한 GCM 메시지를 안드로이드에서 수신하여 알림으로 띄웠습니다

GCM 구현1 - Android

 

 

안드로이드 : http://ghj1001020.tistory.com/783?category=719597

서버 : http://ghj1001020.tistory.com/784?category=756570

 

 

가이드 : https://developers.google.com/cloud-messaging/android/client

 

1. 앱은 GCM 토큰을 가져와서 앱에 저장합니다

2. 서버에서 앱에 저장된 GCM 토큰을 사용하여 메시지를 요청합니다

3. 앱이 GCM 메시지 수신하고 알림을 띄웁니다

* 테스트 용도 이므로 토큰을 서버에 저장하는 부분은 제외했습니다. (서버에서는 디바이스의 GCM 토큰을 하드코딩해서 보냅니다)

 

 

build.gradle
1
2
3
4
5
dependencies {
    ...
    //gcm
    implementation 'com.google.android.gms:play-services-gcm:11.8.0'
}
cs

 

 

AndroidManifest.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
    <uses-permission android:name="android.permission.WAKE_LOCK" />
    
 
    <application>
 
        ...
        <!-- GCM -->
        <!-- GCM으로 부터 message를 받을 수 있도록 권한선언 -->
        <receiver android:name="com.google.android.gms.gcm.GcmReceiver"
            android:exported="true"
            android:permission="com.google.android.c2dm.permission.SEND">
            <intent-filter>
                <action android:name="com.google.android.c2dm.intent.RECEIVE" />
                <action android:name="com.google.android.c2dm.intent.REGISTRATION" />
 
                <category android:name="com.ghj.gcmex" />
            </intent-filter>
        </receiver>
 
        <!-- 등록 토큰 생성 및 업데이트 처리 -->
        <service android:name=".gcm.MyInstanceIDService"
            android:exported="false">
            <intent-filter>
                <action android:name="com.google.android.gms.iid.InstanceID" />
            </intent-filter>
        </service>
 
        <!-- 메시지 수신처리 -->
        <service android:name=".gcm.MyGcmListenerService"
android:exported="false">
            <intent-filter>
                <action android:name="com.google.android.c2dm.intent.RECEIVE" />
                <action android:name="com.google.android.c2dm.intent.REGISTRATION" />
            </intent-filter>
        </service>
 
        <!-- 토큰 가져오기 -->
        <service android:name=".gcm.RegistrationIntentService"
            android:exported="false" />
 
    </application>

cs

* 메시지 수신할 때 절전모드를 유지하기위해 WAKE_LOCK 권한을 줍니다

* com.google.android.gms.gcm.GcmReceiver 은 GCM으로부터 메시지를 받는 리시버입니다

 

 

MyInstanceIDService.java

1
2
3
4
5
6
7
8
9
10
public class MyInstanceIDService extends InstanceIDListenerService {
 
 
    //등록 토큰이 업데이트 되면 호출된다
    @Override
    public void onTokenRefresh() {
        Intent intent = new Intent(this, RegistrationIntentService.class);
        startService(intent);
    }
}
cs

* GCM 토큰이 업데이트 되면 호출되는 서비스 입니다

* 토큰을 가져와서 앱에 저장하기 위해 RegistrationIntentService 서비스를 호출합니다

 

 

RegistrationIntentService.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
//IntentService : 작업을 수행하는 스레드를 별도로 생성 , stopService도 자동적으로 호출
public class RegistrationIntentService extends IntentService {
 
    private static final String TAG = "RegistIntentService";
 
    public RegistrationIntentService()
    {
        super( TAG );
    }
 
 
    //서비스 시작시 호출
    @Override
    protected void onHandleIntent(@Nullable Intent intent)
    {
        String senderId = getString(R.string.gcm_sender_id);
 
        try {
            synchronized ( TAG ) {
                //토큰 가져와서 앱에 저장
                InstanceID instanceID = InstanceID.getInstance(this);
                String token = instanceID.getToken( senderId , GoogleCloudMessaging.INSTANCE_ID_SCOPE , null);
 
                Log.d("GCM_Token" , token);
                saveGcmToken(token);
            }
        } catch ( Exception e ) {
            e.printStackTrace();
        }
    }
 
    //shared preferences 에 토큰 저장
    public void saveGcmToken(String token) {
        SharedPreferences pref = getSharedPreferences(getString(R.string.pref_gcm_token) , Context.MODE_PRIVATE);
        SharedPreferences.Editor editor = pref.edit();
        editor.putString(getString(R.string.pref_key_gcm_token) , token);
        editor.commit();
    }
}
cs

* GCM 토큰을 가져와서 앱에 저장하는 서비스 입니다

 

 

MyGcmListenerService.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
public class MyGcmListenerService extends GcmListenerService {
 
    private final String TAG = "MyGcmListenerService";
 
    //알림
    private final String GCM_DATA_KEY_TITLE = "title";
    private final String GCM_DATA_KEY_BODY = "body";
    private static int NOTIFICATION_ID = 0;
    private final long[] ALARM_PATTERN = new long[]{ 100100100100 };
 
 
    //메시지 수신시 호출
    @Override
    public void onMessageReceived(String s, Bundle bundle) {
        //메시지 수신 후 알림 띄우기
        if( bundle != null ) {
            Log.d(TAG, "onMessageReceived : "+bundle.toString());
 
            String title = bundle.getString(GCM_DATA_KEY_TITLE , "");
            String body = bundle.getString(GCM_DATA_KEY_BODY , "");
            sendNotificaion(title , body);
        }
    }
 
    //알림에 띄우기
    public void sendNotificaion(String title, String body) {
        NotificationManager notiManager = (NotificationManager)getSystemService(Context.NOTIFICATION_SERVICE);
 
        String chGroupId = getString(R.string.alarm_ch_group_id_gcm);
        String channelId = getString(R.string.alarm_ch_id_gcm);
        //26 오레오 이상
        if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            if!checkNotificationChannelGroup(chGroupId) ) {
                //알림 채널 그룹
                NotificationChannelGroup notiGroup = new NotificationChannelGroup( chGroupId, getString(R.string.alarm_ch_group_name_gcm) );
                notiManager.createNotificationChannelGroup( notiGroup );
            }
 
            if!checkNotificationChannel(channelId) ) {
                //알림 채널
                NotificationChannel notiChannel = new NotificationChannel(channelId, getString(R.string.alarm_ch_name_gcm), NotificationManager.IMPORTANCE_HIGH);
                notiChannel.setGroup(chGroupId);
                notiChannel.enableLights(true);
                notiChannel.setLightColor(Color.GREEN);
                notiChannel.enableVibration(true);
                notiChannel.setVibrationPattern( ALARM_PATTERN );
                notiChannel.setLockscreenVisibility(Notification.VISIBILITY_PRIVATE);
                notiChannel.setShowBadge(true);
                notiManager.createNotificationChannel(notiChannel);
            }
        }
 
        //알림 생성
        String appName = getString(R.string.app_name);
 
        Intent intent = new Intent(this, MainActivity.class);
        intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
        PendingIntent pi = PendingIntent.getActivity(this0, intent, PendingIntent.FLAG_UPDATE_CURRENT);   //이미 존재할 경우 이를 유지하고 추가 데이터만 새로운 Intent에 있는 것으로 대체
 
        NotificationCompat.Builder noti = new NotificationCompat.Builder(this, channelId);
        noti.setAutoCancel(true);   //클릭시 알림 제거
        noti.setContentTitle(title);
        noti.setContentText(body);
        noti.setTicker(appName);
        noti.setSmallIcon(R.mipmap.ic_launcher);
        noti.setContentIntent(pi);
        noti.setVibrate( ALARM_PATTERN );
        noti.setSound( RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION) );
        noti.setOngoing(false);  //지우기 버튼으로 삭제 (true - 일반 알림 위에 정렬, 지우기 버튼으로 삭제 못함)
        noti.setOnlyAlertOnce(true);    //알림이 아직 표시되지 않은 경우 사운드, 진동, 티커만 재생되도록
        noti.setWhen( System.currentTimeMillis() ); //알림에서 보여지는 발생시간
        noti.setNumber(1);  //알림의 오른쪽 번호
        noti.setGroupSummary(true);
 
        if ( Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP ) {
            noti.setVisibility( Notification.VISIBILITY_SECRET );   //보안화면에서 알림 보이지 않음
        }
 
        try {
            notiManager.notify(NOTIFICATION_ID++ , noti.build() );
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
 
    //notification channel group 등록 체크
    public boolean checkNotificationChannelGroup(String groupId) {
        boolean result = false//true : 이미 등록됨 , false : 미등록
 
        if ( Build.VERSION.SDK_INT >= Build.VERSION_CODES.O ) {
            NotificationManager notificationManager = (NotificationManager)getSystemService(Context.NOTIFICATION_SERVICE);
            List<NotificationChannelGroup> groups = notificationManager.getNotificationChannelGroups();
 
            if( groups != null && groups.size() > 0 ) {
                for ( NotificationChannelGroup group : groups ) {
                    if( groupId.equalsIgnoreCase(group.getId()) ) {
                        result = true;
                        break;
                    }
                }
            }
        }
 
        return result;
    }
 
    //notification channel 등록 체크
    public boolean checkNotificationChannel( String channelId ) {
        if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.O ) {
            NotificationManager notificationManager = (NotificationManager)getSystemService(Context.NOTIFICATION_SERVICE);
            return notificationManager.getNotificationChannel(channelId) != null ? true : false;
        }
 
        return false;
    }
}
 
cs

* GCM 메시지를 수신한 후 알림을 띄웁니다

* GCM 메시지 수신이 되면 onMessageReceived 함수가 호출됩니다

 

 

MainActivity.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class MainActivity extends AppCompatActivity {
 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
 
        getGcmToken();
    }
 
    //token 가져오기
    public void getGcmToken(){
        SharedPreferences pref = getSharedPreferences(getString(R.string.pref_gcm_token) , Context.MODE_PRIVATE);
        String token = pref.getString(getString(R.string.pref_key_gcm_token), "");
 
        if( TextUtils.isEmpty(token) ) {
            Intent intent = new Intent( this, RegistrationIntentService.class );
            startService( intent );
        }
        else {
            Log.d("GCM_Token", token);
        }
    }
}
cs

* MainActivity 시작시 GCM 토큰이 없으면 GCM 토큰을 가져오는 서비스를 시작합니다

+ Recent posts