728x90

Android Studio 에서 Alt + Enter 키를 누르면 Import 가 자동으로 추가되는데 좀 더 편하게 하는 방법이다.





Updated 2020.4.4

코틀린에서 자동 추가가 안되어 찾아보니 아래와 같이 해주어야 제대로 되는 걸 확인할 수 있었다.


블로그 이미지

Link2Me

,
728x90

Project name 을 변경 시도하다가 실패했다.

그래서 eclipse 에서 Project name 을 변경시도했다.

http://beansarcade.tistory.com/29 를 보고 따라 했더니 금방 해결되었다.


다시 Android Studio 에서 import 로 불러와서 확인해보니 프로젝트명이 정상적으로 잘 되었다.

왜 이름을 변경하느냐? 고 묻는다면 FCM 연동 때문이다.


이제 FCM 연동을 할 차례다.


FCM(Firebase Cloud Message) 를 활용하여 Push 알림 메시지를 구현하고자 한다.

- 서버 게시판에서 특정 글을 등록하면 지정된 회원에게 Push 메시지 자동 전송


Firebase 클라우드 메시징(FCM)은 메시지를 무료로 안정적으로 전송할 수 있는 교차 플랫폼 메시징 솔루션이다.


eclips 에서 4.0.3 버전으로 컴파일 하던 것을 Android Studio 로 import 하니까

build.gradle (Module:App) 에

dependencies {
    compile 'com.android.support:support-v4:18.0.0'

}
라고 표시되었다.


아무 생각없이 신규 프로젝트 생성시 만들어진 걸 복사해서 넣었더니 문제가 발생했다.

알고 보니까 기존 compile 정보를 그대로 유지하면 문제가 되지 않았다.


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:support-v4:18.0.0'
    //compile 'com.android.support:appcompat-v7:25.3.0'
    testCompile 'junit:junit:4.12'

    //추가한 라인
    compile 'com.google.firebase:firebase-messaging:9.6.1'

    //서버로 데이터를 보낼때 okhttp 를 사용한다면 추가
    compile 'com.squareup.okhttp3:okhttp:3.2.0'
}



정작 문제는 FCM 메시지를 전혀 받지 못한다.


요즈음 구글이 GCM 에서 FCM 으로 변경을 권고하면서 GCM 연동하는 방법이 나온 것을 따라서 해보려고 해도 쉽지가 않다.

게다가 eclipse 에서 FCM 연동하는 방법을 아직 잘 모르겠다.

eclipse 버전이 낮으면 FCM 연동도 쉽지 않은거 같다.


그래서 eclipse 를 Android Studio 로 변경하는 것은 완전히 포기하고 새로운 파일을 생성해서 하는 걸로 시도중이다.



블로그 이미지

Link2Me

,
728x90

Avtivity 화면에서
XML로 정의되어 있는 View 와 Layout을 객체화 시키는 메소드가 inflate 다.
inflate 메소드를 통해서 XML 리소스 정보를 해석하여 View를 생성하고 rootView를 리턴한다.
리턴된 rootView를 setContentView() 라는 메소드를 통해 보여줄 수 있다.
setContentView를 이용하면 XML 레이아웃을 인플레이션 한 후 화면에 보여주는 기능을 하기도 하고, 인플레이션 된 위젯을 화면에 구성하는 기능을 한다.

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



XML 로 작성된 메뉴(옵션 메뉴, 컨텍스트 메뉴)는 inflate(팽창 → 프로그래밍 객체로 변환) 하면 실제 메뉴가 생성된다.


기본적인 사용 패턴은

// 1. inflater 얻어오기

MenuInflater inflater = getMenuInflater();

LayoutInflater inflater = getLayoutInflater();

LayoutInflater inflater = (LayoutInflater) getSystemService( Context.LAYOUT_INFLATER_SERVICE );


// 2. View inflate 하기 (XML 파일과 연결 (R. 으로 시작되는 resource 파일들만 inflate 가능))

inflater.inflate(R.menu.mymenu, menu);

View v = (View) inflater.inflate( R.layout.inflate_example, parent, false );


// 3. 화면에 표시하기

setContentView( v );


다시 정리하면

LayoutInflater inflater = (LayoutInflater) getSystemService( Context.LAYOUT_INFLATER_SERVICE );
View v = (View) inflater.inflate( R.layout.inflate_example, parent, false );
setContentView(v);



Main View 에 부분 xml 을 가져와서 보여주는 것에 대한 예제를 보자.


LinearLayout add_layout = (LinearLayout) findViewById(R.id.addLayout);
// 인플레이션
LayoutInflater inflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
inflater.inflate(R.layout.add_layout,add_layout,true); 



=== activity_main.xml ===

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

    <TextView
        android:id="@+id/txt01"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="부분 레이아웃"
        android:layout_marginTop="30dp"
        android:gravity="center_horizontal"
        android:textSize="20dp"
        android:textColor="#a88" />

    <Button
        android:id="@+id/btn1"
        android:layout_gravity="center_horizontal"
        android:text="Add"
        android:textAllCaps="false"
        android:textSize="20dp"
        android:onClick="btn_addlayout"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

    <LinearLayout
        android:id="@+id/addLayout"
        android:orientation="vertical"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

    </LinearLayout>
</LinearLayout>


부분 Layout 을 추가할 add_layout.xml 를 정의한다.

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

    <Button
        android:id="@+id/btnTest"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:gravity="center"
        android:text="동의 토글"
        android:textSize="20dp"
        android:textStyle="bold" />

    <RadioGroup
        android:id="@+id/rg1"
        android:orientation="horizontal"
        android:layout_gravity="center"
        android:layout_margin="10dp"
        android:layout_marginRight="5dp"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content">

        <RadioButton
            android:id="@+id/rb1"
            android:text="독서"
            android:textColor="#00f"
            android:textSize="20dp"
            android:layout_marginRight="5dp"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" />

        <RadioButton
            android:id="@+id/rb2"
            android:text="여행"
            android:textColor="#00f"
            android:textSize="20dp"
            android:layout_marginRight="5dp"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" />

        <RadioButton
            android:id="@+id/rb3"
            android:text="스포츠"
            android:textColor="#00f"
            android:textSize="20dp"
            android:layout_marginRight="5dp"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" />

        <RadioButton
            android:id="@+id/rb4"
            android:text="영화"
            android:textColor="#00f"
            android:textSize="20dp"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" />

    </RadioGroup>

    <LinearLayout
        android:gravity="center"
        android:layout_margin="10dp"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <TextView
            android:text="동의"
            android:textSize="20dp"
            android:layout_margin="10dp"
            android:textColor="#f00"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" />

        <CheckBox
            android:id="@+id/agree"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" />

    </LinearLayout>

</LinearLayout>


이제 MainActivity.java 파일에 inflation 을 시킨다.

import android.content.Context;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.LinearLayout;

public class MainActivity extends AppCompatActivity {

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

    // 버튼을 클릭했을 때 동작처리 메소드
    public void btn_addlayout(View view){
        addLayout();
    }

    // add_layout.xml 을 activity_main.xml 에 추가하는 메소드
    private void addLayout(){
        LinearLayout add_layout = (LinearLayout) findViewById(R.id.addLayout);

        // 인플레이션
        LayoutInflater inflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        inflater.inflate(R.layout.add_layout,add_layout,true);

        // add_layout.xml 에 버튼 객체 참조
        Button btnTest = (Button) findViewById(R.id.btnTest);
        final CheckBox agree = (CheckBox) findViewById(R.id.agree);

        btnTest.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if(agree.isChecked()){
                    agree.setChecked(false);
                } else {
                    agree.setChecked(true);
                }
            }
        });

    }
}



블로그 이미지

Link2Me

,
728x90

사용자가 버튼을 클릭하면, 클릭 이벤트가 발생한다.

Button 요소에 onClick 속성을 추가한다.

클릭 이벤트가 발생하면 onClick 속성에 저장된 메소드가 호출된다.

 

<Button
        android:id="@+id/btn1"
        android:layout_gravity="center_horizontal"
        android:text="Add"
        android:textAllCaps="false"
        android:textSize="20dp"
        android:onClick="btn_addlayout"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

 

 

MainActivity.java 에 클릭 이벤트를 처리하는 메소드를 정의한다.

onClick 명을 btn_addlayout 이라고 했으므로 클릭 이벤트 이름도 동일하게 해주어야 한다.

onClick 속성에 선언하는 메소드는 public 이어야 하고, void 형을 가지며, View 를 메소드의 인수로 가진다.

 

// 버튼을 클릭했을 때 동작처리 메소드
public void btn_addlayout(View view){
    addLayout();
}

 

 

다른 방법은 가장 흔하게 사용하는 방법이다.

이 방식으로 코딩하는 걸 추천한다.

Button button = (Button) findViewById(R.id.btn1);
button.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        addLayout();
    }
});

 

코틀린 에서 처리 방법

val product_name = findViewById<TextView>(R.id.product_name)
val product_count = findViewById<TextView>(R.id.product_count)


product_name.setOnClickListener { _ ->
    
}

 

Android Studio 3.6 이상에서 ViewBinding 처리 방법을 권장하고 있다.

https://link2me.tistory.com/1974 을 참조하시라.

블로그 이미지

Link2Me

,
728x90

Android 패키지 파일을 분할하는 중에 문제가 발생하였습니다.

실행하려는 폰의 앱이 서버에 있는 앱보다 버전이 낮으면 아래와 같은 메시지를 띄운다.


서버와 앱 버전 체크를 하고 나서 파일 다운로드를 시도하면서 이런 메시지를 뿌린다.

Android 패키지 파일을 분할하는 중에 문제가 발생하였습니다.


defaultConfig {
    applicationId "com.android.USBController"
    minSdkVersion 19
    targetSdkVersion 27
    versionCode 3
    versionName "1.2"

}


버전 코드를 더 낮게 해서 컴파일 하는 것은 아닌지 체크를 해봐야 한다.


Android 앱 개발중 Release 버젼으로 .apk 파일을 생성해 모바일에 직접 설치하고 싶을 때
“패키지 파일을 분할하는 중에 문제가 발생하였습니다” 라는 에러가 나면서 설치되지 않는다면
APK 파일의 minSdkVersion (개발중인 안드로이드 버전)이
설치되는 폰의 Android Version 보다 높기 때문이다.
반대로 얘기하면, 실행하려는 폰이 개발된 앱보다 버젼이 낮기 때문이다.

블로그 이미지

Link2Me

,
728x90

class DownloadFileAsync extends AsyncTask<String, String, String> {
    @Override
    protected void onPreExecute() {
        super.onPreExecute();
        showDialog(DIALOG_DOWNLOAD_PROGRESS);
    }

    @Override
    protected void onPostExecute(String unused) {
        dismissDialog(DIALOG_DOWNLOAD_PROGRESS);



해결방법


class DownloadFileAsync extends AsyncTask<String, String, String> {

private ProgressDialog pDialog;

    @Override
    protected void onPreExecute()
    {
        super.onPreExecute();
        pDialog = new ProgressDialog(context);
        pDialog.setMessage("Please wait...");
        pDialog.setIndeterminate(false);
        pDialog.setCancelable(true);
        pDialog.show();
    }

    @Override
    protected void onPostExecute(String unused) {
        pDialog.dismiss();

}

블로그 이미지

Link2Me

,
728x90

아래 코드는 아직 해결이 안된 상태다.

하지만 적어두어야 해결책을 찾으면 수정할 수 있으니 그냥 적는다.


Notification n = new Notification();
n.flags |= Notification.FLAG_AUTO_CANCEL;
// 아이콘 모양
n.icon = R.drawable.icon_small;
// 메시지 내용
n.tickerText = text;
System.out.println("msg======" + text);
// 메시지가 나타나는 시간
n.when = System.currentTimeMillis();
// 메시지의 제목 NotificationCompat.Builder
n.setLatestEventInfo(context,
        context.getResources().getString(R.string.app_name), text,
        pendingIntent(context));
// 메시지를 띄워준다.
nm.notify(mLastId, n);


해결방법

NotificationCompat.Builder 를 사용해서 변경한다.

import android.support.v4.app.NotificationCompat;


NotificationCompat.Builder n = new NotificationCompat.Builder(context);
n.setSmallIcon(R.drawable.icon_small);
n.setTicker(text); //알림이 뜰때 잠깐 표시되는 Text
n.setWhen(System.currentTimeMillis()); // 알림이 표시되는 시간
n.setNumber(10); // 미확인 알림의 개수
n.setContentTitle(context.getResources().getString(R.string.app_name));
n.setContentText(text); //상단바 알림 내용
n.setDefaults(Notification.DEFAULT_SOUND | Notification.DEFAULT_VIBRATE);
n.setContentIntent(pendingIntent(context));
n.setAutoCancel(true); //터치하면 자동으로 지워지도록 설정하는 것

nm.notify(mLastId, n.build()); // 알림을 구분할 상수


import android.support.v4.app.NotificationCompat; 를 했는데 에러메시지가 나오면

project properties->java build path->libraries click add external jar



이렇게 하면 에러메시지는 사라지는데 컴파일을 해서 동작시키면 앱이 죽어버린다.



블로그 이미지

Link2Me

,
728x90

ListView는 사용자가 정의한 데이터 목록을 아이템 단위로 구성하여 화면에 출력하는 ViewGroup의 한 종류다.
즉, ListView 는 어댑터(Adapter)를 통해 데이터를 뿌려주는 형식의 View 다.

ListView는 항목들을 수직으로 보여주는 어댑터 뷰로 상하로 스크롤이 가능하다.

ListView에 표시되는 아이템은 단순히 Text만 출력하는 구조가 될 수 있고, Image, Button, CheckBox 등 여러 View의 조합으로 구성되는 좀 더 복잡한 형태(Custom)가 될 수도 있다.

가장 간단하게 텍스트만 보여주는 예제를 살펴보자.

번호 1, 번호2, 번호 3과의 관계가 어떻게 되는지 아래 코드에서 보면된다.


 3번 화면에 출력할 ListView 추가 (activity_main.xml)

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <ListView
        android:id="@+id/istview_01"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</RelativeLayout>

 1번 배열 리스트 생성 및 2번 어댑터 생성, ListView에 어댑터 연결

 public class MainActivity extends AppCompatActivity {

    ArrayList<String> arrayList = null;
    ArrayAdapter<String> adapter = null;
    ListView listView = null;

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

        // 데이터 배열 리스트 생성
        arrayList = new ArrayList<String>();
        arrayList.add("이순신");
        arrayList.add("홍길동");
        arrayList.add("유관순");
        arrayList.add("강감찬");
        arrayList.add("김유신");
        arrayList.add("강길덕");

        // 어댑터 생성 (안드로이드에서 기본 제공하는 어댑터 활용)
        adapter = new ArrayAdapter<String>(this,android.R.layout.simple_list_item_1,arrayList);

        // AdapterView
        listView = (ListView) findViewById(R.id.istview_01);
        listView.setAdapter(adapter); // ListView 에 어댑터 설정
    }
}


이렇게 데이터 배열 리스트를 생성하는 경우는 없지만 개념으로 알아두자.


ArrayAdapter(Context context, int textViewResourceID, 배열);

- context : 현재 어플리케이션 컨텍스트

- int textViewResourceID : 레이아웃 아이디


ArrayAdapter 클래스는 제네릭 클래스로 정의되어 있다.

android.R.layout.simple_list_item_1 은 안드로이드 시스템 내부에 미리 만들어진 리소스중 하나로 SDK를 설치하면 자동으로 설치되며 어떠한 추가 작업 없이 개발자가 바로 사용할 수 있다.

 android.R.layout.simple_list_item_1

 하나의 텍스트 뷰 사용

 android.R.layout.simple_list_item_checked

 항목당 체크 표시

 listView.setChoiceMode(ListView.CHOICE_MODE_SINGLE)

 listView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE)



이제 데이터 리스트 배열을 만드는 방법을 바꿔보자.


<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string-array name="contact">
        <item>김유신 010-0001-00001</item>
        <item>강감찬 010-0001-00002</item>
        <item>이순신 010-0001-00003</item>
        <item>유관순 010-0001-00004</item>
        <item>홍길동 010-0001-00005</item>
        <item>사임당 010-0001-00006</item>
        <item>최영 010-0001-00007</item>
        <item>이성계 010-0001-00008</item>
        <item>이방원 010-0001-00009</item>
        <item>정몽주 010-0001-00010</item>
        <item>김태성 010-0001-00011</item>
        <item>강길덕 010-0001-00012</item>
    </string-array>
</resources>


 public class MainActivity extends AppCompatActivity {

    ArrayAdapter<CharSequence> adapter = null;
    ListView listView = null;

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

        adapter = ArrayAdapter.createFromResource(
                                this,R.array.contact,android.R.layout.simple_list_item_checked);

        listView = (ListView) findViewById(R.id.listview_01);

        listView.setAdapter(adapter);
        listView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE);
        ColorDrawable colorDrawable = new ColorDrawable(Color.RED);
        listView.setDivider(colorDrawable);
        listView.setDividerHeight(3);
    }
}


블로그 이미지

Link2Me

,
728x90

구글링을 해서 전화수신 기능을 테스트하다보니 ITelephony 에서 에러가 나온다.


해결방법은 첨부파일을 다운로드 받아서 Android Studio app/src/main/ 폴더에서 압축을 풀면 된다.

ITelephony_aidl.zip

app\src\main\java\com\android\internal\telephony\ITelephony.aidl 에 파일이 복사된다.

압축파일은 다른 폴더로 옮기거나 삭제한다.


이제 에러 표시가 되는 곳에서 Alt + Enter 키를 누르면 아래 화면이 나온다. 여기에서 Create interface 'ITelephony' 를 선택한다.



com.android.internal.telephony 를 입력한다.



그러면 Interface 파일이 생성된다.

아래 코드를 복사하여 붙여넣기 한다.

붙여넣기는 http://stackoverflow.com/questions/31473793/where-to-copy-itelephony-java-in-android-studio-and-how-to-import-it-please-don 에서 복사해서 넣으면 된다.


package com.android.internal.telephony;

public interface ITelephony {
    void answerRingingCall();

    boolean endCall();

    void silenceRinger();

    boolean showCallScreenWithDialpad(boolean showDialpad);
}


이렇게 하면 에러 메시지가 사라진다.


블로그 이미지

Link2Me

,
728x90

안드로이드 어플에서 전화수신 팝업 기능을 테스트 해보고자 검색해보니 http://gun0912.tistory.com/46 에 자료가 있다.


코드를 받아서 보니 Butter Knife 라이브러리를 사용했더라.

한번도 사용을 해보지 않은 거라 그런지 소스코드를 붙여넣기 하니 에러가 발생한다.


검색으로 찾아보니 이 분 블로그에 방법이 나온다.

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    testCompile 'junit:junit:4.12'
    compile 'com.android.support:appcompat-v7:23.4.0'
    compile 'com.mcxiaoke.volley:library-aar:1.0.0'
    compile 'com.android.support:support-annotations:24.2.0'
    compile 'com.google.firebase:firebase-messaging:9.6.1'
    // butterknife 추가해주는 것만으로 알아서 import 해 줌
    compile 'com.jakewharton:butterknife:7.0.1'

}

이렇게 하고 나면 Sync Now 를 눌러줘야 한다.


CallingService.java 파일내에 코드에서 에러가 해결되지 않는 곳이 있다.

@InjectView(R.id.tv_call_number) TextView tv_call_number;

ButterKnife.inject(this, rootView);


import butterknife.InjectView;
부분을 인식할 수 없다고 나온다.


문제 해결을 위해서 구글링을 해보니 http://jakewharton.github.io/butterknife/ 에서 확인하라고 나온다.


버전이 높아지면서 기능이 변경되었다는 걸 알 수 있었다.

compile 'com.jakewharton:butterknife:8.5.1'


기 존

수 정

 @InjectView(R.id.tv_call_number)
 TextView tv_call_number;
 @BindView(R.id.tv_call_number)
 TextView tv_call_number;

 ButterKnife.inject(this, rootView);

 ButterKnife.bind(this, rootView);


Gradle 에 버전을 수정해주고 코드를 수정하고나서 에러 메시지가 없어졌다.


이제 컴파일하여 코드를 테스트하는데 코드가 비정상적으로 동작하면서 죽는다.

원인이 정확하게 해결되지 않았나 보다..

원인 해결되면 수정 정리해야겠다.


블로그 이미지

Link2Me

,
728x90

프로세스(process)는 '실행중인 프로그램'이다. 프로그램을 실행하면 OS로부터 실행에 필요한 자원(메모리)를 할당받아 프로세스가 된다.

프로세스는 프로그램을 수행하는 데 필요한 데이터와 메모리 등의 자원 그리고 쓰레드로 구성되어 있으며, 프로세스 자원을 이용해서 실제로 작업을 수행하는 것이 바로 쓰레드이다.

그래서 모든 프로세스에는 최소한 하나 이상의 쓰레드가 존재한다.


안드로이드는 multi-threading 을 지원한다. 즉 하나의 애플리케이션은 동시에 여러가지 작업을 할 수 있다. 안드로이드는 자바의 multi-threading 을 그대로 지원한다.

개발자들은 하나의 애플리케이션 안에 동시에 실행되는 여러 Thread를 만들 수 있으며, 이 Thread 들은 자바 Virtual Machine 에 의해 동시에 실행된다.


애플리케이션이 구동되면 안드로이드 시스템은 애플리케이션을 위한 실행 Thread를 생성한다. 이 Thread는 Main Thread 라고 불리며, UI(user interface) Thread라고 불리기도 한다. Main Thread 는 사용자 인터페이스 위젯에 이벤트를 전달하거나 화면을 그리는 작업을 담당하고 있다.
동일한 Process 안에서 실행된느 모든 컴포넌트는
Main Thread 안에서 실행된다.

그리고 컴포넌트의 시스템 호출도 Main Thread 안에서 이루어진다.

안드로이드 UI는 기본적으로 싱글 쓰레드 모델로 작동한다.
안드로이드의 애플리케이션을 실행하면 시스템은 AndroidManifest.xml 파일에 기록된 최초의 액티비티를 메모리로 올려 프로세스를 만든다. 이때 Main Thread 가 자동으로 생성된다.


안드로이드 Thread 모델은 자바의 Thread 모델을 그대로 따른다.


쓰레드를 구현하는 방법은

Thread Class 를 상속받는 방법과 Runnable 인터페이스를 구현하는 방법이 있다.


Thread Class 를 상속받으면 다른 Class를 상속받을 수 없기 때문에, Runnable 인터페이스를 구현하는 방법이 일반적이다. Runnable 인터페이스를 구현하는 방법은 재사용성이 높고 코드의 일관성을 유지할 수 있어 보다 객체지향적인 방법이라고 할 수 있다.


Runnable 인터페이스를 구현한 경우, Runnable 인터페이스를 구현한 클래스의 인스턴스를 생성한 다음, 이 인스턴스를 Thread 클래스의 생성자의 매개변수로 제공해야 한다.

Runnable runnable = new Runnable();
Thread thread = new Thread(runnable);
thread.setDaemon(true); // 메인 쓰레드 종료시 같이 종료처리
thread.start();
 


쓰레드를 생성했다고 해서 자동으로 실행되는 것은 아니다. start()를 호출해야만 쓰레드가 실행된다.

하나의 쓰레드에 대해 start()가 한 번만 호출될 수 있다.

한번 실행이 종료된 쓰레드는 다시 실행할 수 없다.

실행중인 사용자 쓰레드가 하나도 없을 때 프로그램은 종료된다.


Thread 안에서 직접적으로 UI(user interface) 위젯을 변경하면 안된다.

동기화 문제를 처리하기 위해 안드로이드에서는 Main Thread 에서만 UI 작업이 가능하도록 제한했다.

// Runnable 인터페이스를 구현한 클래스
class Runnable implements java.lang.Runnable {
    @Override
    public void run() {
        while(true){
            backValue++;  // 작업스레드 값 증가
            // 쓰레드 안에서 UI를 조작하면 에러가 발생한다.
            backText.setText("작업스레드 Value: " + backValue);
            try {
                Thread.sleep(10000); // 1000ms 단위로 실행(멈춤)
                System.out.println(Thread.currentThread().getName());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
} // end class Runnable
 


UI Thread가 아닌 다른 Thread 에서 UI를 조작하면, UI 툴킷에 접근하지 말라는 원칙을 위배해서 오류를 발생시킨다.

이 문제를 해결하기 위해서 안드로이드는 다른 Thread(Sub Thread) 에서 UI Thread(Main Thread) 에 접근하는 많은 방법을 제공하고 있다.

- Handler : 일반적인 Thread 간 상호 작용을 위한 클래스

  work Thread가 여러개이고, 서로 비슷한 메시지를 보내야 한다면
  핸들러 클래스를 정의하는 것이 낫다.

- view.post(Runnable())  : UI Thread의 메시지큐에 작업을 추가. 해당 view 객체에 접근 가능한 경우에 사용

- View.postDelayed(Runnable())  : 일정 시간후에 작업처리

- Activity.runOnUiThread(Runnable())  : 핸들러 없이 UI Thread 의 메시지큐에 작업을 전달(post)

  runOnUiThread() 메소드는 Runnable 객체를 넘겨 받는데,
  넘겨 받는 코드가 백그라운드 Thread 이거나 UI Thread 인지 상관없이 무조건 UI 스레드에서 실행

- AsyncTask  : http://link2me.tistory.com/1031 참조



여러가지 방법중에 Handler 를 이용하는 방법에 대해 살펴보자.

UI 를 조작하기 위해서는 Handler와 Looper를 이용해 다른 Thread에서 Main Thread로 UI 처리 작업을 전달하던지, Main Thread 내에서 자체적으로 처리하던지 해야한다.

<이미지 출처 : http://itmining.tistory.com/5>


Handler

안드로이드에서는 Main Thread 를 통해서만 화면 UI를 변경할 수 있기 때문에 핸들러를 통해서 Main Thread 에 접근하여 UI를 수정할 수 있다.

- 핸들러를 생성하는 Thread 만이 다른 Thread 가 전송하는 Message와 Runnable 객체를 받을 수 있다.

- 핸들러가 처리하는 대상은 메시지(Message)와 런어블(Runnable) 객체이며 단일 쓰레드에 단 하나의 루퍼와 메세지 큐가 존재하지만, 핸들러는 다수 존재 가능하다.

- Handler 객체를 새로 생성하해당 쓰레드의 Message Queue에 바인드(bind)된다.

- 자료구조에서 메시지를 쌓아두는 공간이 MessageQueue 이다.

- Looper는 MessageQueue가 비어있는 동안은 아무 행동도 안하고 메시지가 들어오면 해당 메시지를 꺼내 적절한 Handler로 전달한다.

- 쓰레드 간의 통신을 위해서 메시지가 필요하다면 핸들러의 obtainMessage() 메서드를 사용할 수 있다.
obtainMessage() 메서드는 각 메서드에 따라 what, arg1, arg2, obj 등을 초기화할 수 있는 기능을 제공한다
-
Runnable 인터페이스는 run() 추상 메서드를 가지고 있으므로 상속받은 클래스는 run()코드를 반드시 구현해야 한다.


Handler 를 구현하는 방법은 http://netrance.blog.me/110130561171 에 잘 설명되어 있다.


1. 안드로이드는 핸들러를 구현할 수 있도록 Handler라는 베이스 클래스를 제공한다.
2. Handler 가 받을 메시지들의 종류를 결정한다.
3. 자식 Handler 클래스를 어디에 구현할지 결정한다.
   - Activity 등과 같은 특정 클래스의 내부
   - 메소드 내 (익명 클래스로 구현)
   - 별도의 자바 파일
4. 메시지의 종류가 무엇인지 식별한다.
   메시지 코드들을 정의한다.
   // Message Codes
   private static final int MSG_DAY_CHANGED = 1;
   private static final int MSG_MONTH_CHANGED = 2;
   private static final int MSG_YEAR_CHANGED = 3;

5. Message를 전달하는 방식
1. sendMessage() - 메시지를 바로 전달
2. sendMessageAtTime() - 메시지를 특정시간에 전달
3. sendMessageDelayed() - 메시지를 일정시간이 지난뒤에 전달
4. sendMessageAtFrontQueue() - 딜레이된 메시지가 있는경우 가장 앞에 전달


 Handler Class를 생성하여 workThread 내용을 갱신처리

 - 버튼을 클릭하면 Runnable 객체 생성한 Thread가 시작되어 workThread 값이 계속 증가된다.

 - 증가된 값은 Handler 객체를 통해 UI Thread에 값을 표시한다.

Thread_handler-01.zip

import android.os.Handler;
import android.os.Message;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;

public class MainActivity extends AppCompatActivity {

    int mainValue = 0;
    int workValue = 0;
    TextView mainText;
    TextView workText;
    Button button;

    ProgressHandler progressHandler;

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

        mainText = (TextView)findViewById(R.id.mainvalue);
        workText = (TextView)findViewById(R.id.backvalue);
        button = (Button) findViewById(R.id.btn_increase);

        progressHandler = new ProgressHandler();
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                startCount();
            }
        });
    } // end of onCreate

    private void startCount(){
        mainValue++;

        // 2. Runnable 객체  생성으로 구현
        Runnable runnable = new Runnable();
        Thread thread2 = new Thread(runnable);
        thread2.setDaemon(true);
        thread2.start();

        mainText.setText("메인스레드 값: " + mainValue );
    }

    class ProgressHandler extends Handler{
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            if(msg.what == 0){
                // 메인 Thread의 UI 내용 변경
                workText.setText("BackValue:" + workValue);
            }
        }
    }

    // Runnable 인터페이스를 구현한 클래스
    class Runnable implements java.lang.Runnable {
        @Override
        public void run() {
            while(true){
                workValue++;  // 작업스레드 값 증가
                // 쓰레드 안에서 UI를 조작하면 에러가 발생한다.
                //workText.setText("작업스레드 Value: " + workValue);

                // 메인에서 생성된 Handler 객체의 sendEmptyMessage 를 통해 Message 전달
                progressHandler.sendEmptyMessage(0);

                try {
                    Thread.sleep(3000); // 1000ms(1초)이므로 3초 단위로 실행
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    } // end class Runnable

}


progressHandler.sendEmptyMessage(0); 대신에
Message msg = progressHandler.obtainMessage();
progressHandler.sendMessage(msg);
를 쓸 수 있다.


 익명 클래스를 이용한 Handler 객체 생성하여 workThread 내용을 갱신 처리 

Thread_handler-02.zip

import android.os.Handler;
import android.os.Message;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.TextView;

public class MainActivity extends AppCompatActivity {

    int mainValue = 0;
    int backValue = 0;
    TextView mainText;
    TextView backText;

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

        mainText = (TextView)findViewById(R.id.mainvalue);
        backText = (TextView)findViewById(R.id.backvalue);

        // Runnable 객체 생성으로 구현
        Runnable runnable = new Runnable();
        Thread thread = new Thread(runnable);
        thread.setDaemon(true);
        thread.start();
    } // end of onCreate

    public void OnClick(View v){
        mainValue++;
        mainText.setText("메인스레드 값: " + mainValue );
    }

    // Main Thread 에서 Handler 객체를 생성한다.

    // Handler 객체를 생성한 쓰레드 만이 다른 쓰레드가 전송하는 Message 나 Runnable 객체를 수신 가능

    // 익명클래스를 이용한 Handler 객체를 생성
    Handler handler = new Handler(){ //클래스 내부의 클래스, 재사용하지 않을 클래스를 만들때 주로 사용
        // Handler 객체는 handlerMessage() 를 오버라이딩 하여 Message 를 수신
        @Override
        public void handleMessage(Message msg) {
            if(msg.what == 0){   // Message id 가 0 이면
                backText.setText("BackValue:" + backValue); // 메인스레드의 UI 내용 변경
            }
        }
    };

    // Runnable 인터페이스를 구현한 클래스
    class Runnable implements java.lang.Runnable {
        @Override
        public void run() {
            while(true){
                backValue++;  // 작업스레드 값 증가
                // 쓰레드 안에서 UI를 조작하면 에러가 발생한다.
                //backText.setText("작업스레드 Value: " + backValue);
                // 메인에서 생성된 Handler 객체의 sendEmptyMessage 를 통해 Message 전달
                handler.sendEmptyMessage(0);


                try {
                    Thread.sleep(1000); // 1000ms(1초) 단위로 실행
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    } // end class Runnable
}
 


Handler 와 WorkerThread 사용하는 방법


 import android.os.Handler;
import android.os.Message;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;

public class HandlerActivity extends AppCompatActivity {
    Button handlerStartButton;
    Button handlerStopButton;
    Handler mainThreadHandler;
    TextView handlerExampleTextView;
    int workValue = 0;
    boolean running = true;

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

        handlerStartButton = findViewById(R.id.btn_start);
        handlerStopButton = findViewById(R.id.btn_stop);
        handlerExampleTextView = findViewById(R.id.tv);
        mainThreadHandler = new Handler(){
            @Override
            public void handleMessage(Message msg) {
                super.handleMessage(msg);
                if(msg.what == 1){
                    handlerExampleTextView.setText("BackValue:" + workValue); // 메인스레드의 UI 내용 변경
                }
            }
        };

        handlerStartButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                running = true;
                startCount();
            }
        });
        handlerStopButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                running = false;
                workValue = 0;
            }
        });
    }

    private void startCount(){
        // Start a child thread when button is clicked.
        WorkerThread thread = new WorkerThread();
        thread.start();
    }

    private class WorkerThread extends Thread{
        @Override
        public void run() {
            super.run();
            while(running){
                workValue++;  // 작업스레드 값 증가
                try {
                    java.lang.Thread.sleep(1000); // 1000ms(1초)이므로 1초 단위로 실행
                    System.out.println(java.lang.Thread.currentThread().getName());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                // Create a message in child thread.
                Message childThreadMessage = new Message();
                childThreadMessage.what = 1;
                childThreadMessage.arg1 = workValue;
                // Put the message in main thread message queue.
                mainThreadHandler.sendMessage(childThreadMessage);
            }

        }
    }
}


블로그 이미지

Link2Me

,
728x90

하나의 안드로이드 앱에서 다룰 수 있는 메모리의 한계 때문에 큰 사이즈 이미지는 OutofMemory exception이나 퍼포먼스 문제가 생길 수 있다. 작은 뷰의 크기에 맞지 않는 고해상도의 이미지를 불러오는 것은 괜한 리소스를 낭비하게 만든다.


=== BitMap.java ===

 import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.view.View;

public class BitMap extends View {
    public BitMap(Context context) {
        super(context);
    }

    protected void onDraw(Canvas canvas){
        canvas.drawColor(Color.CYAN);

        // Resource 폴더에 저장된 그림파일을 Bitmap 으로 만들어 리턴해준다
        Bitmap bitmap = BitmapFactory.decodeResource(getResources(),R.drawable.car1);
        // 이미지가 너무 크면 화면에서 잘려서 보임
        canvas.drawBitmap(bitmap,10,10,null); // 비트맵 이미지를 캔버스에 출력하는 함수
    }
}

 Bitmap myImage = BitmapFactory.decodeFile(“/sdcard/car1.jpg”);
 BitmapFactory.decodeFile() : 로컬에 존재하는 파일을 그대로 읽어올 때 쓴다.
 파일경로를 파라미터로 넘겨주면 FileInputStream 을 만들어서 decodeStream 을 한다.

 Canvas.drawBitmap() 은 비트맵 이미지를 캔버스에 출력하는 함수
 - 1번째 파라미터는 비트맵 이미지 객체, 2번째 파라미터는 원본 이미지의 영역좌표,
 - 3번째 파라미터는 이미지가 표시되는 화면상의 영역좌표, 4번째 파라미터는 Paint 객체
 - 2번째와 4번째파라미터는 생략 가능하다.

 === MainActivity.java ===

 import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;

public class MainActivity extends AppCompatActivity {

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

        BitMap myView = new BitMap(this);
        setContentView(myView);
    }
}



https://ringsterz.wordpress.com/2014/11/27/imageview%EC%97%90-%EB%8C%80%EC%9A%A9%EB%9F%89-bitmap-%ED%9A%A8%EA%B3%BC%EC%A0%81%EC%9C%BC%EB%A1%9C-%EB%A1%9C%EB%94%A9%ED%95%98%EA%B8%B0/

에 자동으로 줄이는 함수가 있어서 이걸 이용해서 테스트 해봤다.


BitmapFactory.Options : BitmapFactory 가 사용하는 옵션클래스이다.
Options 객체를 생성하고 설정하고자 하는 옵션을 넣은후 BitmapFactory 의 함수 실행시 파라미터로 넘기면된다.
inSampleSize : decode 시 얼마나 줄일지 설정하는 옵션인데 1보다 작을때는 1이 된다.
1보다 큰값일 때 1/N 만큼 이미지를 줄여서 decoding 하게 된다. 보통 2의 배수로 설정한다.


import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Rect;
import android.view.View;

public class BitMap extends View {
    public BitMap(Context context) {
        super(context);
    }

    protected void onDraw(Canvas canvas){
        canvas.drawColor(Color.CYAN);

        // Resource 폴더에 저장된 그림파일을 Bitmap 으로 만들어 리턴해준다
        Bitmap bitmap = BitmapFactory.decodeResource(getResources(),R.drawable.car2);
        // 이미지가 너무 크면 화면에서 잘려서 보임
        canvas.drawBitmap(bitmap,10,10,null);

        Bitmap resize = resize_decodeResource(getResources(), R.drawable.car2, 100, 100);
        canvas.drawBitmap(resize,10,900,null); // 비트맵 이미지를 캔버스에 출력하는 함수
    }

    public static Bitmap resize_decodeResource(Resources res, int resId,

                                                                               int reqWidth, int reqHeight) {

        // First decode with inJustDecodeBounds=true to check dimensions
        final BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeResource(res, resId, options);

        // Calculate inSampleSize
        options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);

        // Decode bitmap with inSampleSize set
        options.inJustDecodeBounds = false;
        return BitmapFactory.decodeResource(res, resId, options);
    }

    public static int calculateInSampleSize(
        BitmapFactory.Options options, int reqWidth, int reqHeight) {
        // Raw height and width of image
        final int height = options.outHeight;
        final int width = options.outWidth;
        int inSampleSize = 1;

        if (height > reqHeight || width > reqWidth) {
            final int halfHeight = height / 2;
            final int halfWidth = width / 2;

            // Calculate the largest inSampleSize value that is a power of 2 and keeps both
            // height and width larger than the requested height and width.
            while ((halfHeight / inSampleSize) > reqHeight
                    && (halfWidth / inSampleSize) > reqWidth) {
                inSampleSize *= 2;
            }
        }
        return inSampleSize;
    }
}


이미지 출력 결과


Bitmap resize = resize_decodeResource(getResources(), R.drawable.car2, 100, 100);

에서 width 와 height 지정된 크기로 이미지가 줄어드는게 아니라 원래 이미지 크기가 width 와 height 중에서 작은 것 기준으로 맞춰서 줄여진다.


그래서 높이 기준으로 줄여주는 걸로 수정을 했다.

protected void onDraw(Canvas canvas){
    canvas.drawColor(Color.CYAN);

    Bitmap resize = autoresize_decodeResource(getResources(), R.drawable.car2, 100);
    canvas.drawBitmap(resize,10,300,null); // 비트맵 이미지를 캔버스에 출력하는 함수
}

public static Bitmap autoresize_decodeResource(Resources res, int resId, int reqHeight) {

    // First decode with inJustDecodeBounds=true to check dimensions
    final BitmapFactory.Options options = new BitmapFactory.Options();
    options.inJustDecodeBounds = true;
    BitmapFactory.decodeResource(res, resId, options);

    // Calculate inSampleSize
    options.inSampleSize = calculateInAutoSize(options, reqHeight);

    // Decode bitmap with inSampleSize set
    options.inJustDecodeBounds = false;
    return BitmapFactory.decodeResource(res, resId, options);
}

public static int calculateInAutoSize(BitmapFactory.Options options, int reqHeight) {
    // 원본 이미지의 높이와 너비
    final int height = options.outHeight;
    final int width = options.outWidth;

    float ratio = width / height;
    int reqWidth = Math.round((float) ratio * width);

    int inSampleSize = 1;
    if (height > reqHeight) {
        final int halfHeight = height / 2;
        while ((halfHeight / inSampleSize) > reqHeight) {
            inSampleSize *= 2;
        }
    }
    return inSampleSize;
} 


블로그 이미지

Link2Me

,
728x90

FCM 테스트를 해보려고 Android Studio 에서 신규로 파일을 생성하는 방식으로 해봤다.

그러다보니 Eclipse 기반에서 구현한 로그인 코드가 에러가 발생해서 로그인이 안된다.

deprecated 된 것을 모두 수정해서 코드를 보완했다.


 === Login.java ===

 package com.tistory.link2me.addresschart;

import android.annotation.TargetApi;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.ProgressDialog;
import android.content.ContentValues;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.provider.Settings.Secure;
import android.support.annotation.NonNull;
import android.support.v4.util.Pair;
import android.telephony.TelephonyManager;
import android.util.Log;
import android.view.KeyEvent;
import android.view.View;
import android.webkit.CookieManager;
import android.webkit.CookieSyncManager;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.EditText;
import android.widget.Toast;

import com.google.firebase.iid.FirebaseInstanceId;

import java.io.BufferedReader;
import java.io.DataOutputStream;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
import java.util.List;
import java.util.Map;

public class Login extends Activity {

    String getDeviceID; // 스마트기기의 장치 고유값
    EditText etId;
    EditText etPw;

    String loginID;
    String loginPW;
    CheckBox autologin;
    Boolean loginChecked;
    public SharedPreferences settings;

    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_login);

        // 네트워크 연결상태 체크
        if(NetworkConnection() == false){
            NotConnected_showAlert();
        }

        etId = (EditText) findViewById(R.id.login_id_edit);
        etPw = (EditText) findViewById(R.id.login_pw_edit);
        autologin = (CheckBox) findViewById(R.id.autologinchk);

        settings = getSharedPreferences("settings", Activity.MODE_PRIVATE);
        loginChecked = settings.getBoolean("LoginChecked", false);
        if (loginChecked) {
            etId.setText(settings.getString("loginID", ""));
            etPw.setText(settings.getString("loginPW", ""));
            autologin.setChecked(true);
        }

        if(!settings.getString("loginID", "").equals("")) etPw.requestFocus();

        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()){
                    new AsyncLogin().execute(loginID,loginPW);
                }
            }
        });

    }

    private  class AsyncLogin extends AsyncTask<String, Void, String> {
        ProgressDialog pdLoading = new ProgressDialog(Login.this);
        HttpURLConnection conn;

        @Override
        protected void onPreExecute() {
            super.onPreExecute();
            //this method will be running on UI thread
            pdLoading.setMessage("\tValidating user...");
            pdLoading.setCancelable(false);
            pdLoading.show();
        }

        @Override
        protected String doInBackground(String... params) {

            try {
                URL url = new URL(Value.IPADDRESS + "/loginChk.php");
                conn = (HttpURLConnection) url.openConnection();
                // 세션 쿠키 전달
                String cookieString = CookieManager.getInstance().getCookie(Value.IPADDRESS);

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

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

                System.out.println("userID : " + loginID);
                System.out.println("Input PW : " + loginPW);
                System.out.println("DeviceID : " + getDeviceID);
                System.out.println("Login Token : " + getToken);

                if(conn != null){ // 연결되었으면
                    conn.setConnectTimeout(10000); // milliseconds 연결 타임아웃시간

                    //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.setUseCaches(false);
                    conn.setDefaultUseCaches(false);
                    conn.setDoOutput(true); // POST 로 데이터를 넘겨주겠다는 옵션
                    conn.setDoInput(true); // 서버로부터 응답 헤더와 메시지를 읽어들이겠다는 옵션

                    // 전달할 인자들
                    Uri.Builder builder = new Uri.Builder()
                            .appendQueryParameter("loginID", params[0])
                            .appendQueryParameter("loginPW", params[1])
                            .appendQueryParameter("deviceID", getDeviceID)
                            .appendQueryParameter("tokenID", getToken);
                    String urlParameters = builder.build().getEncodedQuery();

                    DataOutputStream wr = new DataOutputStream(conn.getOutputStream());
                    wr.writeBytes(urlParameters);
                    wr.flush();
                    wr.close();

                    int responseCode = conn.getResponseCode();
                    System.out.println("GET Response Code : " + responseCode);
                    if(responseCode == HttpURLConnection.HTTP_OK){ // 연결 코드가 리턴되면
                        BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream()));
                        StringBuilder sb = new StringBuilder();
                        String line;
                        while((line = reader.readLine())!= null){
                            sb.append(line);
                        }
                        System.out.println("line Value : " + sb.toString().trim());
                        return sb.toString().trim();
                    }else{
                        return("unsuccessful");
                    }
                }

            } catch(Exception e){
                return new String("Exception: " + e.getMessage());
            } finally {
                conn.disconnect();
            }
            return null;
        }

        protected void onPostExecute(String result){
            pdLoading.dismiss();
            if(result.equalsIgnoreCase("1")){ // 로그인 정보 일치
               Toast.makeText(Login.this,"로그인 성공", Toast.LENGTH_SHORT).show();
                startActivity(new Intent(getApplication(), MainActivity.class));
                finish(); // finish()를 호출해서 Activity를 없애줌

            } else if(result.equalsIgnoreCase("-1")){ // 등록된 단말기와 불일치
                deviceDismatch_showAlert();
            } else if (result.equalsIgnoreCase("0")) {
                showAlert();
            } else {
                Toast.makeText(Login.this, "서버로부터 정보가 잘못 전송되었습니다", Toast.LENGTH_SHORT).show();
            }
        }
    }

    public void onStop(){
        // 어플리케이션이 화면에서 사라질때
        super.onStop();
        // 자동 로그인이 체크되어 있고, 로그인에 성공했으면 폰에 자동로그인 정보 저장
        if (autologin.isChecked()) {
            settings = getSharedPreferences("settings",Activity.MODE_PRIVATE);
            SharedPreferences.Editor editor = settings.edit();

            editor.putString("loginID", loginID);
            editor.putString("loginPW", loginPW);
            editor.putBoolean("LoginChecked", true);

            editor.commit();
        } else {
            // 자동 로그인 체크가 해제되면 폰에 저장된 정보 모두 삭제
            settings = getSharedPreferences("settings",    Activity.MODE_PRIVATE);
            SharedPreferences.Editor editor = settings.edit();
            editor.clear(); // 모든 정보 삭제
            editor.commit();
        }

    }

    public void deviceDismatch_showAlert(){
        AlertDialog.Builder builder = new AlertDialog.Builder(Login.this);
        builder.setTitle("등록단말 불일치");
        builder.setMessage("최초 등록된 단말기가 아닙니다.\n" + "관리자에게 문의하여 단말기 변경신청을 하시기 바랍니다.")
                .setCancelable(false)
                .setPositiveButton("OK", new DialogInterface.OnClickListener() {
                    public void onClick(DialogInterface dialog, int id) {
                        dialog.dismiss();
                    }
                });
        AlertDialog alert = builder.create();
        alert.show();
    }

    public void showAlert(){
        AlertDialog.Builder builder = new AlertDialog.Builder(Login.this);
        builder.setTitle("로그인 에러");
        builder.setMessage("로그인 정보가 일치하지 않습니다.")
                .setCancelable(false)
                .setPositiveButton("OK", new DialogInterface.OnClickListener() {
                    public void onClick(DialogInterface dialog, int id) {
                        dialog.dismiss();
                    }
                });
        AlertDialog alert = builder.create();
        alert.show();
    }

    private void NotConnected_showAlert() {
        AlertDialog.Builder builder = new AlertDialog.Builder(Login.this);
        builder.setTitle("네트워크 연결 오류");
        builder.setMessage("사용 가능한 무선네트워크가 없습니다.\n" + "먼저 무선네트워크 연결상태를 확인해 주세요.")
                .setCancelable(false)
                .setPositiveButton("확인", new DialogInterface.OnClickListener() {
                    public void onClick(DialogInterface dialog, int id) {
                        finish(); // exit
                        //application 프로세스를 강제 종료
                        android.os.Process.killProcess(android.os.Process.myPid() );
                    }
                });
        AlertDialog alert = builder.create();
        alert.show();

    }

    private boolean NetworkConnection() {
        int[] networkTypes = {ConnectivityManager.TYPE_MOBILE, ConnectivityManager.TYPE_WIFI};
        try {
            ConnectivityManager manager = (ConnectivityManager) getSystemService (Context.CONNECTIVITY_SERVICE);
            for (int networkType : networkTypes) {
                NetworkInfo activeNetwork = manager.getActiveNetworkInfo();
                if(activeNetwork != null && activeNetwork.getType() == networkType){
                    return true;
                }
            }
        } catch (Exception e) {
            return false;
        }
        return false;
    }

    // Back 버튼을 누르면 어플 종료여부 확인 처리
    @Override
    public boolean onKeyDown(int keyCode, KeyEvent event) {
        if( keyCode == KeyEvent.KEYCODE_BACK ) {
            new AlertDialog.Builder(this).setIcon(android.R.drawable.ic_dialog_alert).setTitle("Quit").setMessage("어플을 종료하시겠습니까?").setPositiveButton("Yes", new DialogInterface.OnClickListener() {
                @Override
                public void onClick( DialogInterface dialog, int which) {
                    moveTaskToBack(true); // 본Activity finish후 다른 Activity가 뜨는 걸 방지.
                    finish();
                    //application 프로세스를 강제 종료
                    android.os.Process.killProcess(android.os.Process.myPid() );
                }
            }).setNegativeButton( "No", null ).show();

            return true;
        }

        return super.onKeyDown(keyCode, event);
    }

    @Override
    protected void onResume()
    {
        super.onResume();
        if (android.os.Build.VERSION.SDK_INT >= 21) flushCookies();
        else CookieSyncManager.getInstance().startSync();
    }

    @Override
    protected void onPause()
    {
        super.onPause();
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
            //noinspection deprecation
            CookieSyncManager.getInstance().stopSync(); // 동기화 종료
        }
    }

    @TargetApi(21)
    private void flushCookies() {
        // 롤리팝 이상에서는 CookieManager의 flush를 하도록 변경됨.
        CookieManager.getInstance().flush();
    }
}


서버 loginChk.php

- 서버에서는 $_POST 배열로 전달받은 값이면 로그인 처리 함수를 통해서 로그인 성공이면 1을 반환하고 틀리면 0을 반환하도록 코드를 구현하면 된다.


 <?php
if(!isset($_SESSION)) {
    session_start();
}

@extract($_POST); // POST 전송으로 전달받은 값 처리
if(isset($loginID) && !empty($loginID) && isset($loginPW) && !empty($loginPW)) {

    $deviceID = $deviceID ? $deviceID : '';

    if(isset($deviceID) && !empty($deviceID)){ // 모바일 접속이면
        require_once 'db.info.php';
        require_once 'phpclass/dbClass.php';
        $conn=new MySQLiDbClass();
        $dbconn = $conn->isConnectDb($DB); // 안드로이드폰에서는 반드시 객체로 생성해야 정상접속
        require_once 'phpclass/loginClass.php';
        $c=new LoginClass();

        $result = $c->MobileUserAuthCheck($loginID,$loginPW,$deviceID);
        if($result > 0 ) {
            session_save_path('./_tmp/session');

            $_SESSION['userID'] = $loginID;
            $_SESSION['userPW'] = md5($loginPW);
            $_SESSION['ip'] = $_SERVER['REMOTE_ADDR'];
            $_SESSION['ua'] = $_SERVER['HTTP_USER_AGENT'];

            echo $result; // 로그인 성공이면 1을 반환
        } else if($result == 0) {
            echo 0; // 로그인 정보 틀림
        } else {
            echo '-1'; // Phone mismatch
        }

    }

} else {
    echo("<meta http-equiv='Refresh' content='0; URL=loginForm.php'>");
}
?>


유사 게시글(POD 기반 로그인 처리) : http://link2me.tistory.com/1404

블로그 이미지

Link2Me

,
728x90

삼성폰은 쉽게 USB 디버깅을 설정할 수 있는데 LG G5 폰에서 USB 디버깅(개발자모드)를 활성화시키는 방법을 몰라서 고생을 좀 했다.


1. LG USB 통합드라이버를 홈페이지에서 받아서 설치한다.

    http://www.lge.co.kr/lgekor/download-center/downloadCenterList.do

    소프트웨어 다운로드

    LG United Mobile Driver(LG 360 캠 포함)


    ※ Firefox 브라우저에서 다운로드 했더니 죽어도 안되더라. 그래서 Explorer 로 받았더니 받아지네. 우라질 ^^


2. 폰에서 설정변경 방법

    설정 --> 시스템 --> 휴대폰 정보 --> 소프트웨어정보 --> 빌드번호

    빌드번호가 보일 것이다.

    빌드번호를 7번 눌러주면 개발자 모드가 활성화된다.


3. Android Studio 에서 인식이 안된다.

   왜 그런지 도저히 원인을 찾을 수가 없다.


2017.9.21일 추가사항

- 개발자 모드가 활성화되면 폰 인식을 시킬 수 있는 단계까지 준비가 된 거더라.

  최근에 삼성폰 외에 다른 폰에서도 기능이 제대로 동작되는지 확인하기 위해서

  LG폰을 구해서 꼭 테스트를 해야 되어서 테스트를 하다가 인식하도록 하는 걸 찾아냈다.

  삼성폰은 개발자 모드 활성화만 시키면 인식되는데, LG폰은 개발자 모드 ON 으로 꼭 한번 더 해줘야....

- 개발자 옵션으로 일단 들어간다.

  상단에 ON/OFF 하는 버튼이 보인다. 이걸 먼저 눌러야 한다.

  먼저 OFF 로 설정한 다음에 ON으로 다시 설정한다. 

- USB 디버깅을 ON 으로 변경하면 이제 변경이 된다.

  이렇게 하면 폰에서 인식이 된다.

블로그 이미지

Link2Me

,
728x90

안드로이드 Broadcast 를 테스트 해보고 정리해둔다.
개념도 제대로 모른채 구글링해서 나온 예제를 실행해보고 실행이 되는구나 하는 정도만으로는 응용이 불가능하다는 걸 테스트 해보면서 몸소 느끼고 있다. Android Studio 기반에서 테스트한 것을 적어둔다.

안드로이드 6.0 에서 SMS 권한이 없는데 허용할 것인지 문의하는 창도 뜨고 잘 동작된다.


BroadCast.zip


Android Studio 는 main 폴더에 핵심적인 내용이 들어가 있으므로 이것만 파일 첨부하면 이걸 이용해서 다시 작성하기가 쉽다.


BroadcastReceiver는 모든 어플에게 다 전해지는 것이다. (시스템 매니저가 알아서 보내는 것)


// IntentFilter 전달받는 여러 인텐트중 특정 인텐트만 고르겠다는 의도
IntentFilter filter = new IntentFilter(UsbManager.ACTION_USB_DEVICE_ATTACHED);
registerReceiver(mUsbAttachReceiver , filter); // 브로드캐스트리시버 등록


또는

IntentFilter filter = new IntentFilter();
filter.addAction(Intent.ACTION_USB_DEVICE_ATTACHED);
registerReceiver(mUsbAttachReceiver, filter);  // 브로드캐스트리시버 등록


Broadcast Receiver(방송수신자)는 BroadcastReceiver 클래스를 상속받아 작성한다.

Broadcast 메시지가 수신되면 onReceive 메서드가 자동 호출된다.

public class Broadcastreceiver extends BroadcastReceiver {

    private Bundle bundle;
    private SmsMessage currentSMS;
    private String message;

    @Override
    public void onReceive(Context context, Intent intent) {
        if (intent.getAction().equals(Intent.ACTION_BOOT_COMPLETED)) {
            Log.d("onReceive()","부팅완료");
            //Intent i = new Intent(context, ScreenService.class);
            //context.startService(i);
        }


        if (intent.getAction().equals("android.provider.Telephony.SMS_RECEIVED")) {
            Log.d("SMSBroadcastReceiver", "SMS 메시지가 수신되었습니다.");
            //Toast.makeText(context, "문자가 수신되었습니다", Toast.LENGTH_SHORT).show();
            bundle = intent.getExtras();
            if (bundle != null) {
                Object[] pdu_Objects = (Object[]) bundle.get("pdus");
                if (pdu_Objects != null) {
                    for (Object aObject : pdu_Objects) {
                        currentSMS = getIncomingMessage(aObject, bundle);
                        String senderNo = currentSMS.getDisplayOriginatingAddress();
                        message = currentSMS.getDisplayMessageBody();
                        Toast.makeText(context, "senderNum: " + senderNo + " :\n message: " + message, Toast.LENGTH_LONG).show();
                    }
                    this.abortBroadcast();
                    // End of loop
                }
            }
        } // bundle null



AndroidManifest.xml 을 사용하여 Broadcast Receiver 를 등록

- 앱이 실행중이 아니더라도 지정한 Broadcast 메시지를 수신(지정한 Action 발생)되면 Broadcast Receiver 가 자동 실행된다.


동적으로 Broadcast Receiver 를 등록하면, 앱이 실행중일 때만 Broadcast Receiver 가 수행되며, 동적으로 Broadcast Receiver 를 추가/제거가 가능하다.


<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.tistory.link2me.sms_receive">

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

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <receiver
            android:name ="com.tistory.link2me.sms_receive.Broadcastreceiver"
            android:enabled="true">
            <intent-filter android:priority="10000">
                <action android:name="android.intent.action.BOOT_COMPLETED"/>
                <action android:name="android.provider.Telephony.SMS_RECEIVED" />
            </intent-filter>
        </receiver>

    </application>

</manifest>


AndroidManifest.xml에 해당 리시버를 등록하고 intent-filter로 원하는 브로드캐스팅 받을 Action을 등록한다.

즉, intent-filter는 어떤 intent를 필터링해서 들을것인지 설정하는 것이다.

안드로이드 앱에서 알고 싶은 이벤트들이 있을수 있다. 가령
- 배터리 부족
- 네트워크 연결 / 중단
- SD카드 삽집 / 제거
- SMS 수신
- 카메라 버튼이 눌렸을때
- 폰의 날짜, 시간이 변동되었을때
- 비행기 모드 전환시
- 어플 설치/제거
- 재부팅/종료
- 매 분마다 수신


android:enabled="true" : 시스템이 알아서 Broadcastreceiver를 실행한다는 의미다. default 가 true 다.

android:exported="false" : 외부 어플리케이션에서는 사용할 수 없으며 같은 앱 또는 같은 UserId를 가진 놈만 호출할 수 있다는 뜻이다. (이걸 적어두면 메시지 수신이 제대로 안됨)
android:priority : 숫자가 높을수록 우선순위가 높으며, 우선순위가 높은 리시버부터 순차적으로 메세지가 전달된다.


 === Broadcastreceiver.java === 

 package com.tistory.link2me.sms_receive;

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import android.telephony.SmsMessage;
import android.util.Log;
import android.widget.Toast;

public class Broadcastreceiver extends BroadcastReceiver {
    private Bundle bundle;
    private SmsMessage currentSMS;
    private String message;

    @Override
    public void onReceive(Context context, Intent intent) {

        if (intent.getAction().equals(Intent.ACTION_BOOT_COMPLETED)) {
            Log.d("onReceive()","부팅완료");
            //Intent i = new Intent(context, ScreenService.class);
            //context.startService(i);
        }

        if (intent.getAction().equals(Intent.ACTION_SCREEN_ON)) {
            Log.d("onReceive()","스크린 ON");
        }

        if (intent.getAction().equals(Intent.ACTION_SCREEN_OFF)) {
            Log.d("onReceive()","스크린 OFF");
        }

        if (intent.getAction().equals("android.provider.Telephony.SMS_RECEIVED")) {
            Log.d("SMSBroadcastReceiver", "SMS 메시지가 수신되었습니다.");
            Toast.makeText(context, "문자가 수신되었습니다", Toast.LENGTH_SHORT).show();
            bundle = intent.getExtras();
            if (bundle != null) {
                Object[] pdu_Objects = (Object[]) bundle.get("pdus");
                if (pdu_Objects != null) {
                    for (Object aObject : pdu_Objects) {
                        currentSMS = getIncomingMessage(aObject, bundle);
                        String senderNo = currentSMS.getDisplayOriginatingAddress();
                        message = currentSMS.getDisplayMessageBody();
                        Toast.makeText(context, "senderNum: " + senderNo + " :\n message: " + message, Toast.LENGTH_LONG).show();
                    }
                    this.abortBroadcast();
                    // End of loop
                }
            }
        } // bundle null
    }

    private SmsMessage getIncomingMessage(Object aObject, Bundle bundle) {
        SmsMessage currentSMS;
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            String format = bundle.getString("format");
            currentSMS = SmsMessage.createFromPdu((byte[]) aObject, format);
        } else {
            currentSMS = SmsMessage.createFromPdu((byte[]) aObject);
        }
        return currentSMS;
    }
}


문자수신시각 정보 추가시

Date curDate = new Date(currentSMS.getTimestampMillis());
SimpleDateFormat sdf = new SimpleDateFormat( "yyyy-MM-dd HH:mm:ss" );
String receivedDate = sdf.format( curDate );



=== MainActivity.java ===

 package com.tistory.link2me.sms_receive;

import android.Manifest;
import android.content.pm.PackageManager;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.Toast;

public class MainActivity extends AppCompatActivity {

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

        int permissionCheck = ContextCompat.checkSelfPermission(this, Manifest.permission.RECEIVE_SMS);
        if(permissionCheck == PackageManager.PERMISSION_GRANTED){
            Toast.makeText(this, "SMS 수신권한 있음", Toast.LENGTH_SHORT).show();
        } else {
            Toast.makeText(this, "SMS 수신권한 없음", Toast.LENGTH_SHORT).show();
            if(ActivityCompat.shouldShowRequestPermissionRationale(this,Manifest.permission.RECEIVE_SMS)){
                Toast.makeText(this, "SMS 권한 설정이 필요함", Toast.LENGTH_SHORT).show();
            } else {
                // 권한이 할당되지 않았으면 해당 권한을 요청
                ActivityCompat.requestPermissions(this,new String[] {Manifest.permission.RECEIVE_SMS},1);
            }
        }
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
        if (requestCode == 1) {
            for (int i = 0; i < permissions.length; i++) {
                if (grantResults[i] == PackageManager.PERMISSION_GRANTED) {
                    Toast.makeText(this, permissions[i] + " 권한이 승인됨.", Toast.LENGTH_LONG).show();
                } else {
                    Toast.makeText(this, permissions[i] + " 권한이 거부됨.", Toast.LENGTH_LONG).show();
                }
            }
        }
    }

}



블로그 이미지

Link2Me

,
728x90

Eclipse 에서 만든 코드를 Android Studio 로 변환하고 나서 신규로 코드를 추가하면 문제가 생긴다.

FCM 을 이용한 메시지 발송시 메시지 수신하는 걸 해결하지 못한 상태다.

FCM 메시지 발송을 위한 TokenID 를 회원DB에 저장하는 방법을 Login.java 코드를 이용하여 구현했다.


Login.java 파일에서 로그인 정보를 입력하고 확인을 누르면,  POST 방식으로 서버에 있는 loginChk.php 파일로 전송되어 내용을 확인하고 정보를 기록하고 결과값을 반환한다.

서버에서 결과값은 echo 문을 다시 안드로드이드폰에서 받아서 그 결과값을 기준으로 로그인 성공, 로그인 실패, 인증된 단말인지 여부 체크 등을 한다.


사용자 기기 토큰 정보를 획득하는 코드는 http://blog.naver.com/PostView.nhn?blogId=cosmosjs&logNo=220739141098&categoryNo=0&parentCategoryNo=56&viewDate=&currentPage=1&postListTopCurrentPage=1&from=search 를 참조하면서 해보는 중이다.


=== Login.java ====

package com.tistory.link2me.addresschart;

import android.app.ActionBar;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.ProgressDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.net.ConnectivityManager;
import android.os.Bundle;
import android.provider.Settings.Secure;
import android.telephony.TelephonyManager;
import android.util.Log;
import android.view.KeyEvent;
import android.view.View;
import android.webkit.CookieManager;
import android.webkit.CookieSyncManager;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.EditText;
import android.widget.Toast;

import com.google.firebase.iid.FirebaseInstanceId;

import org.apache.http.NameValuePair;
import org.apache.http.client.HttpClient;
import org.apache.http.client.ResponseHandler;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.cookie.Cookie;
import org.apache.http.impl.client.BasicResponseHandler;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.protocol.HTTP;

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

public class Login extends Activity {

    String getDeviceID; // 스마트기기의 장치 고유값
    ProgressDialog dialog = null;
    EditText etId;
    EditText etPw;

    String loginID;
    String loginPW;
    CheckBox autologin;
    Boolean loginChecked;
    List<NameValuePair> params;
    public SharedPreferences settings;
    CookieManager cookieManager;

    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.login);

        // ActionBar 제거하기
        ActionBar actionbar = getActionBar();
        actionbar.hide();

        // 네트워크 연결상태 체크
        if(NetworkConnection() == false){
            NotConnected_showAlert();
        }

        etId = (EditText) findViewById(R.id.login_id_edit);
        etPw = (EditText) findViewById(R.id.login_pw_edit);
        autologin = (CheckBox) findViewById(R.id.autologinchk);

        settings = getSharedPreferences("settings", Activity.MODE_PRIVATE);
        loginChecked = settings.getBoolean("LoginChecked", false);
        if (loginChecked) {
            etId.setText(settings.getString("loginID", ""));
            etPw.setText(settings.getString("loginPW", ""));
            autologin.setChecked(true);
        }

        if(!settings.getString("loginID", "").equals("")) etPw.requestFocus();

        CookieSyncManager.createInstance(this);
        cookieManager = CookieManager.getInstance();
        CookieSyncManager.getInstance().startSync();

        Button submit = (Button) findViewById(R.id.login_btn);
        submit.setOnClickListener(new Button.OnClickListener(){

            @Override
            public void onClick(View v) {
                dialog = ProgressDialog.show(Login.this, "", "Validating user...", true);
                 new Thread(new Runnable() {
                        public void run() {
                            login();
                        }

                      }).start();
            }

        });

    }

    void login() {
        try {
            loginID = etId.getText().toString().trim();
            loginPW = etPw.getText().toString().trim();

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

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

            String postURL = Value.IPADDRESS + "/loginChk.php";
            HttpPost post = new HttpPost(postURL);

            // 전달할 인자들
            params = new ArrayList<NameValuePair>();
            params.add(new BasicNameValuePair("loginID", loginID));
            params.add(new BasicNameValuePair("loginPW", loginPW));
            params.add(new BasicNameValuePair("deviceID", getDeviceID));
            params.add(new BasicNameValuePair("tokenID", getToken));

            UrlEncodedFormEntity ent = new UrlEncodedFormEntity(params,HTTP.UTF_8);
            post.setEntity(ent);

            HttpClient httpclient = new DefaultHttpClient();

            ResponseHandler<String> responseHandler = new BasicResponseHandler();
            final String responsePost = httpclient.execute(post, responseHandler);

            System.out.println("DeviceID : " + getDeviceID);
            System.out.println("Login Token : " + getToken);
            System.out.println("Response : " + responsePost);

            runOnUiThread(new Runnable() {
                public void run() {
                    dialog.dismiss();
                }
            });

            if(responsePost.equalsIgnoreCase("1")){ // 로그인 정보 일치
                runOnUiThread(new Runnable() {
                    public void run() {
                        Toast.makeText(Login.this,"Login Success", Toast.LENGTH_SHORT).show();
                    }
                });

                // 쿠키 정보 생성을 통한 PHP 세션 접속 유지 처리
                List<Cookie> cookies = ((DefaultHttpClient)httpclient).getCookieStore().getCookies();
                if (!cookies.isEmpty()) {
                    for (int i = 0; i < cookies.size(); i++) {
                        String cookieString = cookies.get(i).getName() + "="
                                    + cookies.get(i).getValue();
                        Log.e("PHP_setCookie", cookieString);
                        cookieManager.setCookie(Value.IPADDRESS, cookieString);
                    }
                }
                Thread.sleep(300);

                startActivity(new Intent(this.getApplicationContext(), MainActivity.class));
                finish(); // finish()를 호출해서 Activity를 없애줌

            } else if(responsePost.equalsIgnoreCase("-1")){ // 등록된 단말기와 불일치
                deviceDismatch_showAlert();
            } else {
                showAlert();
            }
        } catch(Exception e) {
            dialog.dismiss();
            System.out.println("Exception : " + e.getMessage());
        }

    }

    public void onStop(){
        // 어플리케이션이 화면에서 사라질때
        super.onStop();
        // 자동 로그인이 체크되어 있고, 로그인에 성공했으면 폰에 자동로그인 정보 저장
        if (autologin.isChecked()) {
            settings = getSharedPreferences("settings",Activity.MODE_PRIVATE);
            SharedPreferences.Editor editor = settings.edit();

             editor.putString("loginID", loginID);
             editor.putString("loginPW", loginPW);
             editor.putBoolean("LoginChecked", true);

             editor.commit();
        } else {
            // 자동 로그인 체크가 해제되면 폰에 저장된 정보 모두 삭제
            settings = getSharedPreferences("settings", Activity.MODE_PRIVATE);
            SharedPreferences.Editor editor = settings.edit();
            editor.clear(); // 모든 정보 삭제
            editor.commit();
        }

    }

    public void deviceDismatch_showAlert(){
        Login.this.runOnUiThread(new Runnable() {
            public void run() {
                AlertDialog.Builder builder = new AlertDialog.Builder(Login.this);
                builder.setTitle("등록단말 불일치");
                builder.setMessage("최초 등록된 단말기가 아닙니다.\n" + "관리자에게 문의하여 단말기 변경신청을 하시기 바랍니다.")
                       .setCancelable(false)
                       .setPositiveButton("OK", new DialogInterface.OnClickListener() {
                           public void onClick(DialogInterface dialog, int id) {
                           }
                       });
                AlertDialog alert = builder.create();
                alert.show();
            }
        });
    }

    public void showAlert(){
        Login.this.runOnUiThread(new Runnable() {
            public void run() {
                AlertDialog.Builder builder = new AlertDialog.Builder(Login.this);
                builder.setTitle("로그인 에러");
                builder.setMessage("로그인 정보가 일치하지 않습니다.")
                       .setCancelable(false)
                       .setPositiveButton("OK", new DialogInterface.OnClickListener() {
                           public void onClick(DialogInterface dialog, int id) {
                           }
                       });
                AlertDialog alert = builder.create();
                alert.show();
            }
        });
    }

    private void NotConnected_showAlert() {
        AlertDialog.Builder builder = new AlertDialog.Builder(Login.this);
        builder.setTitle("네트워크 연결 오류");
        builder.setMessage("사용 가능한 무선네트워크가 없습니다.\n" + "먼저 무선네트워크 연결상태를 확인해 주세요.")
               .setCancelable(false)
               .setPositiveButton("확인", new DialogInterface.OnClickListener() {
                   public void onClick(DialogInterface dialog, int id) {
                       finish(); // exit
                        //application 프로세스를 강제 종료
                        android.os.Process.killProcess(android.os.Process.myPid() );
                   }
               });
        AlertDialog alert = builder.create();
        alert.show();

    }

    private boolean NetworkConnection() {
        ConnectivityManager manager = (ConnectivityManager) getSystemService (Context.CONNECTIVITY_SERVICE);
        boolean isMobileAvailable = manager.getNetworkInfo(ConnectivityManager.TYPE_MOBILE).isAvailable();
        boolean isMobileConnect = manager.getNetworkInfo(ConnectivityManager.TYPE_MOBILE).isConnectedOrConnecting();
        boolean isWifiAvailable = manager.getNetworkInfo(ConnectivityManager.TYPE_WIFI).isAvailable();
        boolean isWifiConnect = manager.getNetworkInfo(ConnectivityManager.TYPE_WIFI).isConnectedOrConnecting();

        if ((isWifiAvailable && isWifiConnect) || (isMobileAvailable && isMobileConnect)){
            return true;
        }else{
            return false;
        }
    }

    // Back 버튼을 누르면 어플 종료여부 확인 처리
    @Override
    public boolean onKeyDown(int keyCode, KeyEvent event) {
        if( keyCode == KeyEvent.KEYCODE_BACK ) {
            new AlertDialog.Builder(this).setIcon(android.R.drawable.ic_dialog_alert).setTitle("Quit").setMessage("어플을 종료하시겠습니까?").setPositiveButton("Yes", new DialogInterface.OnClickListener() {
                @Override
                public void onClick( DialogInterface dialog, int which) {
                    moveTaskToBack(true); // 본Activity finish후 다른 Activity가 뜨는 걸 방지.
                    finish();
                    //application 프로세스를 강제 종료
                    android.os.Process.killProcess(android.os.Process.myPid() );
                }
        }).setNegativeButton( "No", null ).show();

        return true;
      }

      return super.onKeyDown(keyCode, event);
     }

    @Override
    protected void onResume()
    {
        super.onResume();
        CookieSyncManager.getInstance().startSync();
    }

    @Override
    protected void onPause()
    {
        super.onPause();
        CookieSyncManager.getInstance().stopSync(); // 동기화 종료
    }
}
 


이제 MySQL DB 및 PHP 코드를 구현해야 한다.

MySQL DB에는 tokenID 칼럼을 추가한다. Varchar(200) 으로 길이를 설정해준다. 토콘의 길이가 상당히 길더라.

테스트하는 회원 테이블은 회원ID와 회원 Data 를 분리하여 저장하는 방식으로 만들어보고 있다.


테이블 구조

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 ;


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,
  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; 



=== loginChk.php ===

<?php
if(!isset($_SESSION)) {
    session_start();
}

@extract($_POST); // POST 전송으로 전달받은 값 처리
if(isset($loginID) && !empty($loginID) && isset($loginPW) && !empty($loginPW)) {

    $deviceID = $deviceID ? $deviceID : '';

    if(empty($deviceID)){
        require_once 'dbconnect.php'; // db접속
        require_once 'phpclass/loginClass.php';

        $c=new LoginClass();

        $row = $c->WebUserAuthCheck($loginID,$loginPW);
        if(is_array($row)) {
            if($row['code'] > 0) {
                $_SESSION['userID'] = $row['id'];
                $_SESSION['userPW'] = md5($loginPW);
                $_SESSION['code'] = $row['code'];
                $_SESSION['ip'] = $_SERVER['REMOTE_ADDR'];
                $_SESSION['ua'] = $_SERVER['HTTP_USER_AGENT'];

                echo("<meta http-equiv='Refresh' content='0; URL=m/list.php'>");
            } else {
                echo '권한 불가';
            }
        } else if($row == '0'){
            $msg ='정보가 올바르지 않습니다';
            echo "<script>alert('".$msg."');history.go(-1);</script>";
        } else {
            $msg ='정보가 올바르지 않습니다';
            echo "<script>alert('".$msg."');history.go(-1);</script>";
        }

    } else {
        require_once 'db.info.php';
        require_once 'phpclass/dbClass.php';
        $conn=new MySQLiDbClass();
        $dbconn = $conn->isConnectDb($DB); // 안드로이드폰에서는 반드시 객체로 생성해야 정상접속
        require_once 'phpclass/loginClass.php';
        $c=new LoginClass();

        $result = $c->MobileUserAuthCheck($loginID,$loginPW,$deviceID);
        if($result > 0 ) {
            session_save_path('./_tmp/session');

            $_SESSION['userID'] = $loginID;
            $_SESSION['userPW'] = md5($loginPW);
            $_SESSION['ip'] = $_SERVER['REMOTE_ADDR'];
            $_SESSION['ua'] = $_SERVER['HTTP_USER_AGENT'];

            // 토큰 등록 및 갱신
            if(isset($tokenID) && strlen($tokenID) > 63){
                $c->registerToken($tokenID,$loginID,$deviceID); // DB에 토큰 저장
                //$myfile = fopen("tokenfile.txt", "a") or die("파일 쓰기를 할 수 없습니다.");
                //fwrite($myfile, $tokenID);
                //fclose($myfile);
            }
            echo 1;
        } else if($result == 0) {
            echo 0; // 로그인 정보 틀림
        } else {
            echo '-1'; // Phone mismatch
        }
    }

} else {
    echo("<meta http-equiv='Refresh' content='0; URL=loginForm.php'>");
}
?>
 

 // 토큰 등록 및 갱신(2017-03-24) 함수 만들기 (LoginClass.php 파일내에 존재)
    function registerToken($token,$userID,$deviceID){
        global $dbconn;
        $rs = $this->getDeviceChk($userID,$deviceID);
        if($rs > 0 ){
            $gettokenID = $this->getTokenIDChk($rs); // 0 또는 토큰 반환
            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;
                }
            }
        } else { // 장치 ID 와 다르거나 없으면
            // 로그인 금지
        }
    }

    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;
        }
    }


토큰을 회원 DB에 저장하는 코드 구현로직이 어떻게 되는지만 참고하면 된다고 본다.


FCM 을 이용한 PUSH 메시지 전송을 성공하면 다시 기록해 두련다.

블로그 이미지

Link2Me

,
728x90

fragment란 activity안에 속해있는 화면으로 새로운 activity를 띄우는게 아니라,

하나의 activity에서 fragment만 바꿔가면서 보여주기 때문에

한 activity화면에서 여러개의 화면을 볼 수 있게 해주는 것이라고 할 수 있다.

 

https://developer.android.com/guide/components/fragments.html

 

본 예제는 두 개의 Fragment 를 생성하고 하나의 Fragment 만 Activity 상에 보이도록 할 것이다.
그리고, 액티비티의 버튼을 누르면 다른 Fragment 로 교체되도록 하는 내용에 대해서 적어둔다.

 

fragment_main.zip
다운로드

 

=== fragment01.xml ===

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

    <TextView
        android:id="@+id/tv01"
        android:text="프레그먼트01 입니다"
        android:textSize="20dp"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

    <Button
        android:id="@+id/btn01"
        android:text="Fragment02 로 이동"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

</LinearLayout>

 

=== fragment02.xml ===

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

    <TextView
        android:id="@+id/tv02"
        android:text="프레그먼트02 입니다"
        android:textSize="20dp"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

    <Button
        android:id="@+id/btn02"
        android:text="Fragment 01로 이동"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

</LinearLayout>

 

=== activity_main.xml ===

<?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:id="@+id/parentView"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >

    <fragment
        android:id="@+id/frag01"
        android:name="com.tistory.link2me.fragment.Fragment01"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</RelativeLayout>

 

=== Fragment01.java ===

package com.tistory.link2me.fragment;

import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;

public class Fragment01 extends Fragment {

    public View onCreateView(LayoutInflater inflater, ViewGroup parentView, Bundle savedInstanceState){
        // 인플레이트(inflate)를 한다는 것은 동작 가능한 view 객체로 생성한다는 의미
        ViewGroup rootView = (ViewGroup) inflater.inflate(R.layout.fragment01, parentView,false);

        Button btn = (Button) rootView.findViewById(R.id.btn01);
        btn.setOnClickListener(new View.OnClickListener(){

            @Override
            public void onClick(View v) {
                MainActivity mainActivity = (MainActivity) getActivity();
                mainActivity.onFragmentChanged(0);
            }
        });

        return rootView;
    }
}

 

=== Fragment02.java ===

package com.tistory.link2me.fragment;

import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;

public class Fragment02 extends Fragment {
    public View onCreateView(LayoutInflater inflater, ViewGroup parentView, Bundle savedInstanceState){

        ViewGroup rootView = (ViewGroup) inflater.inflate(R.layout.fragment02, parentView,false);

        Button btn = (Button) rootView.findViewById(R.id.btn02);
        btn.setOnClickListener(new View.OnClickListener(){

            @Override
            public void onClick(View v) {
                MainActivity mainActivity = (MainActivity) getActivity();
                mainActivity.onFragmentChanged(1);
            }
        });

        return rootView;
    }
}

 

=== MainActivity.java ===

package com.tistory.link2me.fragment;

import android.support.v4.app.Fragment;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;

public class MainActivity extends AppCompatActivity {

    Fragment01 fragment01;
    Fragment02 fragment02;

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

        // 초기 지정은 frag01 로

        fragment01 = (Fragment01) getSupportFragmentManager().findFragmentById(R.id.frag01);
        fragment02 = new Fragment02();
    }

    public void onFragmentChanged(int index){
        if(index == 0){
            getSupportFragmentManager().beginTransaction().replace(R.id.parentView,fragment02).commit();
        } else if(index == 1){
            getSupportFragmentManager().beginTransaction().replace(R.id.parentView,fragment01).commit();
        }
    }
}

 

 

'안드로이드 > Layout' 카테고리의 다른 글

Android Fragment 기본 예제  (0) 2018.09.11
FloatingActionButton(FAB)  (0) 2018.08.15
LinearLayout weight  (0) 2018.03.01
Android ViewFlipper(뷰플리퍼)  (0) 2017.05.02
안드로이드 Layout  (0) 2016.07.23
블로그 이미지

Link2Me

,
728x90

Android Studio 상에서 프로젝트 이름(package name)을 변경하고 싶을 때가 있다.


왼쪽 프로젝트 탐색기에서 Android로 변경되어 있는지 확인



java 폴더 선택하여 그 하위에 있는 변경할패키지명을 선택한 후
Shift + F6(Refactor → rename)를 누른다






androidMainfest.xml에서 변경되어 있는지 확인


build.gradle 파일에서 applicationId를 변경할 패키지명으로 변경되었는지 확인


이렇게 하고 컴파일을 하면 패키지명이 변경되어 컴파일 된다.

하지만 물리적인 이름까지 변경되지는 않는다.


물리적인 이름까지 변경해서 테스트를 해보니 에러가 발생하고 제대로 동작이 안되는 거 같다.

그래서 딱 위 그림까지면 변경하면 생성되는 apk 파일명도 변경된다.

블로그 이미지

Link2Me

,
728x90

Android Studio 에서 메뉴 만드는 방법이다.




메뉴를 추가한다.







일일이 입력하지 않고 특정 단축어만 입력하면 자동으로 팝업되면서 선택할 수 있게 나온다.

그러면 엔터키만 치면 그 다음에 입력할 사항만 입력하면 된다.



여기서 에러메시지를 보여주면서 자동으로 넘어가지 않는다.

Android Studio 버전 2.3 에서는 자동으로 완성시키더라.

계속 버전업을 하면서 편리하게 자동완성 기능을 추가하고 있는거 같다.


==== menu_item.xml ====

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">
  <item
      android:id="@+id/item01"
      android:title="메뉴1"
      android:orderInCategory="2"
      android:icon="@android:drawable/ic_lock_idle_low_battery"
      app:showAsAction="always"
      />.

    <item
        android:id="@+id/item02"
        android:title="메뉴2"
        android:orderInCategory="3"
        android:icon="@android:drawable/btn_plus"
        app:showAsAction="always"
        app:actionLayout="@layout/custom_layout"
        />

    <item
        android:id="@+id/item03"
        android:title="메뉴3"
        android:orderInCategory="1"
        android:icon="@android:drawable/sym_def_app_icon"
        app:showAsAction="always"
        />
</menu>


==== custom_layout.xml ===

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

    <EditText
        android:id="@+id/custom_et01"
        android:inputType="text"
        android:imeOptions="actionDone"
        android:layout_width="100dp"
        android:layout_height="wrap_content" />

    <TextView
        android:id="@+id/custom_tv01"
        android:text="찾기"
        android:textSize="15dp"
        android:textColor="#22a"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

</LinearLayout>


==== activity_main.xml ===

<?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"
    tools:context="com.tistory.link2me.Menu.MainActivity">

    <TextView
        android:id="@+id/tv01"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerHorizontal="true"
        android:layout_centerVertical="true"
        android:textSize="18dp"
        android:textColor="#a84"
        android:text="Hello World!" />

    <Button
        android:id="@+id/btn01"
        android:layout_centerHorizontal="true"
        android:layout_below="@id/tv01"
        android:layout_marginTop="20dp"
        android:text="ActionBar 아이콘 변화"
        android:onClick="btnClicked"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

</RelativeLayout>


==== MainActivity.java ====

package com.tistory.link2me.Menu;

import android.support.v7.app.ActionBar;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.KeyEvent;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.inputmethod.InputMethodManager;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;

public class MainActivity extends AppCompatActivity {

    TextView textView;
    EditText editText;
    ActionBar actionBar;

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

        actionBar = getSupportActionBar();
        actionBar.setSubtitle("서브타이틀");

        textView = (TextView) findViewById(R.id.tv01);
    }

    public void btnClicked(View v){
        actionBar.setLogo(android.R.drawable.sym_action_call);
        actionBar.setDisplayOptions(android.app.ActionBar.DISPLAY_SHOW_HOME|android.app.ActionBar.DISPLAY_USE_LOGO);
    }

    // 시스템 상에서 자동으로 호출하는 메소드 및 custom_layout 객체화
    public boolean onCreateOptionsMenu(Menu menu){
        getMenuInflater().inflate(R.menu.menu_item,menu);

        // custom_Layout 객체를 참조
        View findView = menu.findItem(R.id.item02).getActionView();
        if(findView != null){
            editText = (EditText) findView.findViewById(R.id.custom_et01);
            if(editText != null){
                editText.setOnEditorActionListener(findListener);
            }
        } else {
            Toast.makeText(getApplicationContext(), "액션뷰가 없네요", Toast.LENGTH_SHORT).show();
        }
        return true;
    }

    public boolean onOptionsItemSelected(MenuItem item){
        switch (item.getItemId()){
            case R.id.item01:
                Toast.makeText(this, "메뉴1", Toast.LENGTH_SHORT).show();
                break;

            case R.id.item02:
                Toast.makeText(this, "메뉴2", Toast.LENGTH_SHORT).show();
                break;

            case R.id.item03:
                Toast.makeText(this, "메뉴3", Toast.LENGTH_SHORT).show();
                break;
        }

        textView.setText(item.getTitle().toString());
        return super.onOptionsItemSelected(item);
    }

    private TextView.OnEditorActionListener findListener = new TextView.OnEditorActionListener() {
        @Override
        public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
            if(event == null || event.getAction() == KeyEvent.ACTION_UP){
                find();

                // 키패드 처리 객체 --> InputMethodManager 객체
                InputMethodManager inputMethodManger = (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE);
                // 키패드 닫기
                inputMethodManger.hideSoftInputFromWindow(textView.getWindowToken(),0);
                textView.setText("메뉴2");
            }
            return true;
        }
    };

    private void find(){
      String findString = editText.getEditableText().toString();
        Toast.makeText(this, "검색어 : "+findString, Toast.LENGTH_SHORT).show();
    }
 
}





블로그 이미지

Link2Me

,
728x90

어플을 Android 5.1 버전에서는 문제없이 잘 동작했는데 6.0에서 테스트를 하니까 로그인 자체가 안된다.


해결방법으로 간단한 방법을 알아보자.



 

target SDK 가 23이상이면 안된다.

인스톨시 버전을 낮춰야 한다. 버전이 높으면 위와 같이 직접 수동으로 처리해주는 것이 최선의 방법인거 같다.

블로그 이미지

Link2Me

,