728x90

RecyclerView Adapter 를 별도 Adapter 파일로 만들어서 Activity 간 데이터 처리를 해보고 기록해둔다.

그동안 Inner Class 로 하나의 Activity 내에서 처리하는 것만 해봤는데 Activity간 처리하는 것이라 고려할 사항이 좀 된다.


RecyclerView 는 android.support.v7.widget.CardView 와 같이 사용하면 깔끔한 화면을 볼 수 있다.

또한, view 를 두개 이상 선택적으로 보여주는 것도 편하더라.

Intent 로 화면 전환 처리하는 걸 ContentAdapter 에서 처리하는 걸 문제없이 처리하기 위해 구글링으로 여러 자료를 참조하고 완성된 결과를 얻었다.


두 파일간에 Interface 를 통해 처리를 한다.

OnItemClickListene 를 인터페이스라고 하며 class 가 아닌 interface 라는 키워드를 이용하여 작성한다.

onItemClick함수를 인터페이스 함수라고 한다.

public interface OnItemClickListener { // Class 처럼 상속받아올 인터페이스명
    void onItemClick(View v, int position); // 추상 메소드명
}


자바의 인터페이스 개념 이해를 위해서 구글링을 해보니 기본적인 Interface 개념만 나온다.

안드로이드 인터페이스 예제로 검색해야 원하는 걸 얻을 수 있다.

http://yujuwon.tistory.com/entry/%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C-%EC%9D%B4%EB%B2%A4%ED%8A%B8-%EB%A6%AC%EC%8A%A4%EB%84%88-%EB%A7%8C%EB%93%A4%EA%B8%B0

http://codeasy.tistory.com/2?category=751348


안드로이드 인터페이스 구성요소 : https://kairo96.gitbooks.io/android/content/ch3.1.html 참조


안드로이드 Interface 를 사용한 예제 중에서 https://gist.github.com/riyazMuhammad/1c7b1f9fa3065aa5a46f 를 참조하면 도움된다. 단, mViewHolder.getPosition() 는 테스트해보니 deprecated 되었다고 동작이 되지 않는다.

position 은 getAdapterPosition() 로 넘기면 정상적으로 원하는 결과를 얻을 수 있다.


본 게시글에서는 Content_Item.java, XML 파일은 모두 생략되어 있으니 구현 로직만 참고하면 많은 도움된다.


Custom View 또는 RecyclerView

public class ContentAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>  {
    private static final int TYPE_ONE = 1;
    private static final int TYPE_TWO = 2;
    Context mContext;
    private ArrayList<Content_Item> IvList;
    Content_Item cItem;
    private OnItemClickListener listener; // 이벤트 리스너를 변수로 선언

    public interface OnItemClickListener { // 인터페이스 정의
        void onItemClick(View v, int position);
    }

    public ContentAdapter(Context mContext, ArrayList<Content_Item> items, OnItemClickListener mOnClickListener) {
        this.mContext = mContext;
        IvList = items;
        this.listener = mOnClickListener;
    }

    // determine which layout to use for the row
    @Override
    public int getItemViewType(int position) {
        Content_Item item = IvList.get(position);
        if (item.getIsFolder().equals("1")) {
            return TYPE_ONE;
        } else if (item.getIsFolder().equals("0")) {
            return TYPE_TWO;
        } else {
            return -1;
        }
    }

    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        // 새로운 뷰를 만든다.
        if (viewType == TYPE_ONE) {
            View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.group_item, parent, false);
            return new ViewHolder1(view);
        } else if (viewType == TYPE_TWO) {
            View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.person_item, parent, false);
            return new ViewHolder2(view);
        } else {
            throw new RuntimeException("The type has to be ONE or TWO");
        }
    }

    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
        // ListView의 getView 부분을 담당하는 메소드
        switch (holder.getItemViewType()) {
            case TYPE_ONE:
                ((ViewHolder1) holder).bind(IvList.get(position), listener);
                break;
            case TYPE_TWO:
                ((ViewHolder2) holder).bind(IvList.get(position), listener);
                break;
            default:
                break;
        }
    }

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

    public class ViewHolder1 extends RecyclerView.ViewHolder {
        public ImageView mImage;
        public TextView mTitle;

        public ViewHolder1(View itemView) {
            super(itemView);
            // 화면에 표시될 View 로부터 위젯에 대한 참조 획득
            mImage = (ImageView) itemView.findViewById(R.id.cell_image);
            mTitle = (TextView) itemView.findViewById(R.id.cell_text);
        }

        public void bind(Content_Item item, final OnItemClickListener listener) {
            cItem = item;
            if(cItem.getIsFolder().equals("1")){
                // 아이템 내 각 위젯에 데이터 반영
                mImage.setImageResource(R.drawable.group_btn);
                mTitle.setText(cItem.getName());
                itemView.setOnClickListener(new View.OnClickListener() {
                    @Override
                    public void onClick(View view) {
                        listener.onItemClick(view,getAdapterPosition());
                    }
                });
            }
        }

    }

    public class ViewHolder2 extends RecyclerView.ViewHolder {
        public ImageView mImage;
        public TextView mTitle;
        public TextView msubTitle1;
        public TextView msubTitle2;

        public ViewHolder2(View itemView) {
            super(itemView);
            mImage = (ImageView) itemView.findViewById(R.id.cell_image);
            mTitle = (TextView) itemView.findViewById(R.id.cell_text);
            msubTitle1 = (TextView) itemView.findViewById(R.id.cell_text_sub1);
            msubTitle2 = (TextView) itemView.findViewById(R.id.cell_text_sub2);
        }

        public void bind(Content_Item item, final OnItemClickListener listener) {
            cItem = item;
            if(cItem.getIsFolder().equals("0")){
                // 아이템 내 각 위젯에 데이터 반영
                String imageUri = cItem.getPhoto();
                if (imageUri.equals("")) {
                    mImage.setImageBitmap(R.drawable.photo_base);
                } else {
                    Glide.with(itemView.getContext()).load(imageUri).into(mImage);
                }
                mTitle.setText(cItem.getName());
                msubTitle1.setText(cItem.getCode());
                msubTitle2.setText(cItem.getPosition());

                itemView.setOnClickListener(new View.OnClickListener() {
                    @Override
                    public void onClick(View view) {
                        listener.onItemClick(view,getAdapterPosition());
                    }
                });
            }
        }
    }
}

public class RecyclerviewList extends AppCompatActivity implements View.OnClickListener {
    Context context;

    private RecyclerView listView; // 리스트뷰
    private ArrayList<Content_Item> cItemList = new ArrayList<>();
    RecyclerView.Adapter contentListAdapter;
    RecyclerView.LayoutManager layoutManager;

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

        // Adapter에 추가 데이터를 저장하기 위한 ArrayList
        listView = (RecyclerView) findViewById(R.id.recyclerview);
        listView.setHasFixedSize(true);
        // Set Layout Manager
        LinearLayoutManager layoutManager = new LinearLayoutManager(this);
        layoutManager.setOrientation(LinearLayoutManager.VERTICAL);
        listView.setLayoutManager(layoutManager);

        contentListAdapter = new ContentAdapter(getApplicationContext(),cItemList, new ContentAdapter.OnItemClickListener(){

            @Override
            public void onItemClick(View v, int position) {
                if(cItemList.get(position).getIsFolder().equals("1")){
                    Toast.makeText(context, "Item Clicked" + position, Toast.LENGTH_LONG).show();
                } else if(cItemList.get(position).getIsFolder().equals("0")){
                    Intent intent = new Intent(RecyclerviewList.this, StaffView.class);
                    intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
                    intent.putExtra("idx", cItemList.get(position).getIdx());
                    intent.putExtra("title", cItemList.get(position).getName());
                    startActivity(intent);
                }
            }
        }); // Adapter 생성
        listView.setAdapter(contentListAdapter); // 어댑터를 리스트뷰에 세팅

        // 서버에서 데이터 가져오기
        getJSONData();
    }

}


검색으로 찾은 자료들이 대부분 개념만 설명되어 완성된 결과를 얻을 수가 없어 삽질을 한참 했다.

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

Android Interface AsyncTask 예제  (0) 2019.11.05
Java Interface Example  (0) 2019.09.05
Java 인터페이스(interface) 개요  (0) 2019.08.20
Android Interface 예제 ★★★  (0) 2018.08.22
Android Interface 예제 1  (0) 2018.08.20
블로그 이미지

Link2Me

,
728x90

앱의 데이터가 자동으로 백업될 수 있다는 걸 몰랐다.

출처 : https://m.blog.naver.com/PostView.nhn?blogId=netrance&logNo=221224761858&targetKeyword=&targetRecommendationCode=1


안드로이드 6.0부터는 AndroidManifest.xml 파일에서 application 요소의 android:allowBackup 속성을 자동으로 true로 설정된다.
이로 인해 사용자도 모르게 데이터가 구글 클라우드에 자동으로 백업된다.


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

            <category android:name="android.intent.category.LAUNCHER" />
        </intent-filter>
    </activity>
</application>

블로그 이미지

Link2Me

,
728x90

recyclerview with multiple view types example 로 검색하면 검색되는 게시글이 https://hashcode.co.kr/questions/561/recyclerview%EC%97%90-%EC%97%AC%EB%9F%AC%EA%B0%9C%EC%9D%98-%EB%B7%B0%ED%83%80%EC%9E%85%EC%9D%84-%EB%84%A3%EC%9D%84-%EC%88%98-%EC%9E%88%EC%9D%84%EA%B9%8C%EC%9A%94 로 나온다.

이와 비슷한 답변이 여기저기 검색된다. 나같은 초보한테는 전혀 도움이 안된다.

완성된 예제 코드를 봐야 약간 활용할 수 있게 된다.


https://www.loopwiki.com/ui-ux-design/recyclerview-with-header-and-footer-android-example/


http://www.sunilandroid.com/2016/11/multiple-view-type-in-recyclerview-in.html


https://guides.codepath.com/android/Heterogenous-Layouts-inside-RecyclerView


직접 테스트해본 것은 아니지만 괜찮은 코드로 보인다.


http://www.codexpedia.com/android/android-recyclerview-with-multiple-different-layouts/

이 게시글을 참조하고 나서 원하는 결과를 구현할 수 있었다.


코드를 구현하면서 Type_1, Type_2 가 서로 내용이 다르고, 서버에서 가져올 데이터의 코드도 서로 상이하여 고민이 많았다.

하지만 이런 코드를 검색하면서 ArrayList 는 1개로 통일하여 설계를 하고 Class 를 만들어야 한다는 점이다.

서버에서 가져올 코드에서 최대한 일치할 수 있는 것을 일치하고 없는 칼럼은 공백으로 저장을 하더라도 하나의 규격으로 Class를 만들어서 활용하면 View Type 를 서로 다르게 보여주는 것이 가능하다.


테스트에 사용된 dependencies 는 아래와 같다.

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])

    implementation 'com.android.support:appcompat-v7:27.1.1'
    implementation 'com.android.support.constraint:constraint-layout:1.1.2'
    implementation 'com.loopj.android:android-async-http:1.4.9' // 서버와의 통신 라이브러리
    implementation 'com.github.bumptech.glide:glide:3.8.0' // 이미지 라이브러리
    implementation 'com.android.support:recyclerview-v7:24.2.0' // ListView 개선 버전
    implementation "com.android.support:cardview-v7:24.2.0"
}  ===> Android Studio 3.1.3


dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile 'com.android.support:appcompat-v7:26.+'
    compile 'com.android.support.constraint:constraint-layout:1.0.2'
    compile 'com.loopj.android:android-async-http:1.4.9' // 서버와의 통신 라이브러리
    compile 'com.github.bumptech.glide:glide:3.8.0' // 이미지 라이브러리
    compile 'com.android.support:recyclerview-v7:24.2.0' // ListView 개선 버전
    compile "com.android.support:cardview-v7:24.2.0"
===> Android Studio 2.3.3


블로그 이미지

Link2Me

,
728x90

인터넷을 검색하여 사용자가 만든 라이브러리를 사용하여 이미지 로딩 처리를 하기도 하는데 이미지 로딩 라이브러리를 사용하는 것이 좋을 거 같아서 테스트를 해보고 있다.


Glide는 안드로이드 이미지 로딩 라이브러리로, 빠르고 효율적인 미디어 관리 오픈소스이고 이미지 로딩 프레임워크다. 미디어 디코딩, 메모리와 디스크 캐싱, 리소스 풀링을 간단하고 쉽게 할 수 있도록 도와준다.


사용법

Glide.with(this).load("http://goo.gl/gEgYUd").into(imageView);

- RecyclerView.Adapter listViewAdapter 에서는 this 대신에 context 사용


테스트 환경

Android Studio 2.3.3 버전 ==> 노트북 스펙 부족으로 최신 3.1.3 으로 업그레이드 중지


compileSdkVersion 26
buildToolsVersion "26.0.0"

defaultConfig {
    applicationId "com.tistory.link2me.asynchttpjson"
    minSdkVersion 19
    targetSdkVersion 26
    versionCode 1
    versionName "1.0"

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


dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile 'com.android.support:appcompat-v7:26.+'
    compile 'com.android.support.constraint:constraint-layout:1.0.2'
    compile 'com.loopj.android:android-async-http:1.4.9' // 서버와의 통신 라이브러리
    compile 'com.github.bumptech.glide:glide:3.8.0' // 이미지 라이브러리
    compile 'com.android.support:recyclerview-v7:24.2.0' // ListView 개선 버전
    compile "com.android.support:cardview-v7:24.2.0"
}


https://github.com/bumptech/glide 에 가면 최신버전이 4.7.1 로 나오는데 이걸 사용하면 buildToolsVersion 을 최신으로 사용하라고 나온다. 그래서 4.6.0 으로 해서 컴파일 해봤는데 recyclerview 에서 제대로 처리가 안되는지 에러가 발생하면서 앱이 종료된다. 4.5.0 4.2.0 으로도 시도했으나 실패된다.

glide:3.6.0, 3.7.0, 3.8.0 으로 한 이후에 문제없이 정상동작된다. ==> Android Studio 3.1.3 에서도 정상 동작된다.

자세히 공부하기 싫어서 검색하는 건 포기.....

https://bumptech.github.io/glide/doc/migrating.html


implementation 'com.squareup.picasso:picasso:2.5.2' // 이미지 라이브러리

Picasso 라이브러리를 사용하는데 에러가 발생하여 Glide 라이브러리를 사용해본 건데 아무 버전이나 잘 동작되는건 아닌거 같다.


라이브러리는 버전업이 되면서 오히려 버그가 발생하기도 해서 가능하면 사용하고 싶지 않은데, 코드가 너무 길어지고 메모리 에러 발생 등 고려할 요인들이 너무 많아서 잘 만들어진 라이브러리를 사용하는 것이 속 편하다.


참고하면 도움되는 글

https://d2.naver.com/helloworld/429368


블로그 이미지

Link2Me

,
728x90

PHP 서버와 안드로이드 클라이언트간에 연동 테스트를 하는데 결과가 표시되지 않는다.

서버를 직접 실행해서 메시지를 확인해보니...

Warning: mysql_fetch_array() expects parameter 1 to be resource, boolean given

와 같은 메시지가 나온다.


이 메시지 나오는 원인은 SQL 문의 칼럼이 잘못된 곳이 있을 경우에 나온다.

echo $sql.'<br />'; 로 결과를 화면에 찍어보고 잘못 오타가 난 칼럼 또는 칼럼에 전혀 없는 명칭을 사용하고 있지는 않는지 확인해보는 것이 좋다.

블로그 이미지

Link2Me

,
728x90

CardView를 사용하는 가장 중요한 이유는 깔끔하고 이뻐서다!

안드로이드 CardView 는 support.v7.widget 에 속한 라이브러리로 SDK 21버전부터 CardView가 추가되었다.

Android RecyclerView 를 사용하면서 CardView 를 같이 사용 할 수 있다.

하나의 RecyclerView 내부에 CardView가 들어가 있는 형태라고 보면 된다.


기본적인 설명은 https://www.journaldev.com/10024/android-recyclerview-android-cardview-example-tutorial 사이트를 참조하면 도움된다.


dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'com.android.support:appcompat-v7:27.1.1'
    implementation 'com.android.support.constraint:constraint-layout:1.1.2'
    implementation 'com.loopj.android:android-async-http:1.4.9' // 서버와의 통신 처리
    implementation 'com.squareup.picasso:picasso:2.5.2' // 이미지 라이브러리
    implementation 'com.android.support:recyclerview-v7:24.2.0' // ListView 개선 버전
    implementation "com.android.support:cardview-v7:24.2.0"
}


android:layout_margin="1dp" 로 했더니 깔끔하지 않아서 android:layout_marginBottom="1dp" 로 처리

cardCornerRadius : 레이아웃에 모서리 반지름 설정

cardBackgroundColor : 카드의 배경색을 설정

contentPadding : 자식 뷰 사이의 내부 간격을 조정

cardElevation : 그림자가 있는 카드를 생성


<android.support.v7.widget.CardView
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/cardview"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_marginBottom="1dp"
    app:cardCornerRadius="1dp">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        android:padding="12dp"
        android:paddingLeft="10dp"
        android:paddingRight="10dp">

        <ImageView
            android:id="@+id/list_cell_image"
            android:layout_width="32dp"
            android:layout_height="32dp"
            android:layout_gravity="center"
            android:layout_marginLeft="10dp"
            android:src="@drawable/expose_btn_n" />

        <LinearLayout
            android:layout_width="100dp"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:layout_marginLeft="20dp"
            android:layout_weight="4"
            android:orientation="vertical">

            <TextView
                android:id="@+id/list_cell_text"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:text="타이틀"
                android:textSize="20dp"
                android:textStyle="bold" />

        </LinearLayout>
    </LinearLayout>
</android.support.v7.widget.CardView>



API 28 이상에서는

<android.support.v7.widget.CardView 대신에 <androidx.cardview.widget.CardView 로 변경해야 한다.


앱 build.gradle 에서도

implementation 'androidx.cardview:cardview:1.0.0' 를 사용해야 한다.

블로그 이미지

Link2Me

,
728x90

Android Studio 3.0.3 에서 업데이트를 시도하면 fonts.xml 파일이 수정되었다고 업그레이드가 계속 거부되어 포기하고 있다가, 다른 PC에서 새로 Android Studio 3.1.3 을 설치한 폴더를 별도 폴더를 만들어서 실행했더니 내부적으로 코드를 업데이트하더니 잘되던 프로젝트 코드가 전혀 인식도 안되고 동작이 안된다.


지금으로서는 괜히 업그레이드를 했나 싶을 정도로 맨붕 상태다.

한동안 안드로이드 코드는 들여다보지 않다가 시도하려니 그나마 알던 것도 기억 저편에서도 사라졌는지 하나도 생각나지 않는다.


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


2018년까지만 compile 로 dependencies 사용이 가능하고 그 이후에는 무조건 implementation 을 사용해야 한다는 내용이 나온다.


모듈 import 를 해도 기존 버전 파일을 인식하지 못하여 폴더 생성을 자동으로 하지 못한다.

폴더를 그냥 복사해서 넣으면 이미 있다고 나온다.

검색해보니 gradle-wrapper.properties 에서 4.4 로 되어 있는데 아래와 같이 변경하란다.

distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-all.zip 으로 변경 했더니 에러 발생한다.

시간이 엄청 오래 걸린다. 다시 버전을 4.5 로 변경하고 Sync now 를 눌러서 재빌드 중이다.

4.5로의 변경은 성공이라고 나온다. 하지만 모듈 import 는 실패된다. 실제 폴더에 파일은 복사되어 있다.

Minimum supported Gradle version is 4.4. Current version is 4.3.

그래서 원복으로 4.4 로 변경하고 포기했다.


The specified Android SDK Build Tools version (26.0.2) is ignored, as it is below the minimum supported version (27.0.3) for Android Gradle Plugin 3.1.3.
Android SDK Build Tools 27.0.3 will be used.


신규로 Module를 생성하는 것은 잘된다. 그래서 모듈을 별도 생성하면서 처리하는 중이다.





블로그 이미지

Link2Me

,
728x90

HttpURLConnection 에서 PHP 세션 헤더 정보를 추출하는 코드를 적어둔다.

완벽하게 완성된 코드를 만들려면 테스트가 좀 더 필요하다.

PHP 서버와 통신하기 위해 헤더 정보를 추출해서 통신하는 것이 꼭 필요한지 여부를 검토해야 한다.

일반적으로 이런 헤더가 없어도 통신하는데 전혀 지장이 없다.

다만, 보안문제에 더 완벽한 방법이 있을까 하는 측면에서 이런 저런 검토를 해보는 중이다.

http://link2me.tistory.com/1493 Android Asynchronous Http Client 코드와 비교해보는 것도 좋을 거 같다.

PHP 세션 헤더를 추출할 일은 없지만 서버와의 통신하는 코드가 더 편리한 것을 이용하면 될 거 같아서 이런 저런 코드 테스트 한 걸 적어둔다.


관련 코드

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.net.NetworkInfo;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.provider.Settings;
import android.support.v7.app.AppCompatActivity;
import android.telephony.TelephonyManager;
import android.widget.Toast;

import com.google.firebase.iid.FirebaseInstanceId;
import com.tistory.link2me.common.AES256Cipher;

import java.io.BufferedReader;
import java.io.DataOutputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;

public class Intro extends AppCompatActivity {
    Boolean loginChecked;
    public SharedPreferences settings;
    String loginID;
    String loginPW;
    String getDeviceID; // 스마트기기의 장치 고유값
    private static final String SESSION_COOKIE = "sessionid";

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

        // 네트워크 연결 검사
        if(NetworkConnection() == false){
            NotConnected_showAlert();
        }

        // 업그레이드 유무 검사

        // 자동 로그인 체크 검사
        settings = getSharedPreferences("settings", Activity.MODE_PRIVATE);
        loginChecked = settings.getBoolean("LoginChecked", false);
        if (loginChecked) {
            loginID = settings.getString("loginID", "");
            loginPW = settings.getString("loginPW", "");
            if(loginID != null && !loginID.isEmpty() && loginPW != null && !loginPW.isEmpty()){
                new AsyncLogin().execute(loginID,loginPW);
            } else {
                startActivity(new Intent(getApplication(), Login.class));
                finish();
            }
        } else{
            startActivity(new Intent(getApplication(), Login.class));
            finish();
        }

    }

    private  class AsyncLogin extends AsyncTask<String, Void, String> {
        ProgressDialog pdLoading = new ProgressDialog(Intro.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();
                // 단말기의 ID 정보를 얻기 위해서는 READ_PHONE_STATE 권한이 필요
                TelephonyManager mTelephony = (TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE);
                if (mTelephony.getDeviceId() != null){
                    getDeviceID = mTelephony.getDeviceId();  // 스마트폰 기기 정보
                } else {
                    getDeviceID = Settings.Secure.getString(getApplicationContext().getContentResolver(), Settings.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");
                    conn.setUseCaches(false);
                    conn.setDefaultUseCaches(false);
                    conn.setDoOutput(true); // POST 로 데이터를 넘겨주겠다는 옵션
                    conn.setDoInput(true); // 서버로부터 응답 헤더와 메시지를 읽어들이겠다는 옵션

                    // 전달할 인자들
                    params[0] = AES256Cipher.AES_Encode(params[0], Value.AES256Key);
                    params[1] = AES256Cipher.AES_Encode(params[1], Value.AES256Key);

                    Uri.Builder builder = new Uri.Builder()
                            .appendQueryParameter("loginID", params[0])
                            .appendQueryParameter("loginPW", params[1])
                            .appendQueryParameter("deviceID", getDeviceID)
                            //.appendQueryParameter("phoneVersion", Build.VERSION.RELEASE)
                            //.appendQueryParameter("phoneBrand", Build.BRAND)
                            //.appendQueryParameter("phoneModel", Build.MODEL)
                            .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){ // 연결 코드가 리턴되면
                        // PHP 세션 헤더 정보 추출
                        String cookieTemp = conn.getHeaderField("Set-Cookie");
                        System.out.println("PHP Set-Cookie : " + cookieTemp);
                        if (cookieTemp.length() > 0) {
                            String[] splitCookie = cookieTemp.split(";");
                            String[] splitSessionId = splitCookie[0].split("=");
                            cookieTemp = splitSessionId[1];
                            SharedPreferences.Editor editor = settings.edit();
                            editor.putString(SESSION_COOKIE, cookieTemp);
                            editor.commit();
                            System.out.println("PHP SESSION_COOKIE : " + cookieTemp);
                        }
                        // 본문 정보 추출
                        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("JSON 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(Integer.parseInt(result) > 0){ // 로그인 정보 일치하면 uid 정보를 받음
                System.out.println("uid value : " + result);
                SharedPreferences.Editor editor = settings.edit();
                editor.putString("idx", result);
                Toast.makeText(Intro.this,"로그인 성공", Toast.LENGTH_SHORT).show();
                startActivity(new Intent(getApplication(), MainActivity.class));
                finish(); // 현재 Activity 를 없애줌

            } else {
                startActivity(new Intent(getApplication(), Login.class));
                finish();
            }
        }
    }

    public void NotConnected_showAlert() {
        AlertDialog.Builder builder = new AlertDialog.Builder(Intro.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();
    }

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



블로그 이미지

Link2Me

,
728x90

android 의 비동기 HTTP 라이브러리 Asynchronous Http Client 에 대한 자료는

http://loopj.com/android-async-http/

http://yasirameen.com/2016/05/asynchronous-http-client-android/

https://java2android1108.blogspot.com/2018/01/android-web-services-using-android.html

를 참조하면 기본적인 사용법은 알 수 있다.

한글 설명은 http://edoli.tistory.com/91 를 참조하면 된다.

예제 설명이 잘된 자료는 http://yasirameen.com/2016/05/asynchronous-http-client-android/ 다.


아래 예제는 자동 로그인하는 Intro.java 코드다. 사용해보니 코드가 심플하고 편한 거 같다.

import cz.msebera.android.httpclient.Header;

public class Intro extends AppCompatActivity {
    Boolean loginChecked;
    public SharedPreferences settings;
    String loginID;
    String loginPW;
    String getDeviceID; // 스마트기기의 장치 고유값

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

        // 자동 로그인 체크 검사
        settings = getSharedPreferences("settings", Activity.MODE_PRIVATE);
        loginChecked = settings.getBoolean("LoginChecked", false);
        if (loginChecked) {
            loginID = settings.getString("loginID", "");
            loginPW = settings.getString("loginPW", "");
            if(loginID != null && !loginID.isEmpty() && loginPW != null && !loginPW.isEmpty()){
                AsyncHttpComm(loginID,loginPW);
            } else {
                startActivity(new Intent(getApplication(), Login.class));
                finish();
            }
        } else{
            startActivity(new Intent(getApplication(), Login.class));
            finish();
        }
    }

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

        RequestParams params = new RequestParams();
        params.put("loginID", Value.encrypt(loginID));
        params.put("loginPW", Value.encrypt(loginPW));
        params.put("deviceID", getDeviceID);


        AsyncHttpClient client = new AsyncHttpClient();
        client.post(Value.IPADDRESS + "/loginChk.php", params, new TextHttpResponseHandler() {

            @Override
            public void onFailure(int statusCode, Header[] headers, String responseString, Throwable throwable) {
                System.out.println("statusCode : " + statusCode);
                System.out.println("responseString : " + responseString);
            }

            @Override
            public void onSuccess(int statusCode, Header[] headers, String responseString) {
                System.out.println("Response Value : " + responseString);
                if(Integer.parseInt(responseString) > 0){ // 로그인 정보 일치하면 uid 정보를 받음
                    SharedPreferences.Editor editor = settings.edit();
                    editor.putString("idx", responseString);
                    Toast.makeText(Intro.this,"로그인 성공", Toast.LENGTH_SHORT).show();
                    startActivity(new Intent(getApplication(), MainActivity.class));
                    finish(); // 현재 Activity 를 없애줌

                } else {
                    startActivity(new Intent(getApplication(), Login.class));
                    finish();
                }
            }
        });
    }

}


Android Studio 3.X 버전에서는 코드 자동완성이 잘 되는데 2.3.3 에서는 코드 자동완성이 잘 안된다.

관련 헤더 정보와 코드를 적어둔다.

import com.loopj.android.http.AsyncHttpClient;
import com.loopj.android.http.JsonHttpResponseHandler;
import com.loopj.android.http.RequestParams;

import org.json.JSONObject;

import cz.msebera.android.httpclient.Header;

public class MainActivity extends AppCompatActivity {

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

        // 서버에서 데이터 가져오기
        getJSONFromAsyncHttpClient();

    }

    public void getJSONFromAsyncHttpClient(){
        RequestParams params = new RequestParams();
        params.put("key", "value");
        params.put("more", "data");
        AsyncHttpClient client = new AsyncHttpClient();
        String url = Value.IPADDRESS + "/orgJSONList.php";
        client.get(url, params, new JsonHttpResponseHandler() {
            @Override
            public void onSuccess(int statusCode, Header[] headers, JSONObject response) {
                // Handle resulting parsed JSON response here
            }

            @Override
            public void onFailure(int statusCode, Header[] headers, String res, Throwable t) {

            }
        });
    }

} 


2019.7.20일자로 1.4.10 버전이 나왔다.

https://loopj.com/android-async-http/

블로그 이미지

Link2Me

,
728x90

Apache 2.4 이상에서 Port-Based Virtual Host 설정하는 방법이다.


httpd.conf 파일 수정사항

Listen 80
Listen 8080


# Apache 2.4 이상에서는 NameVirutal Host 설정 불필요


DocumentRoot "/usr/local/apache/htdocs"
<Directory "/usr/local/apache/htdocs">
    Options FollowSymLinks
    AllowOverride None
    Require all granted
</Directory>


# Virtual hosts
# 비활성화되어 있는 걸 주석제거하고 활성화 처리
Include conf/extra/httpd-vhosts.conf

# Virtual Host 를 추가하면 아래 경로를 추가하고 Virtual Host 파일에서도 추가해야 한다.
<Directory "/home/httpd/abc/www/htdocs">
     AllowOverride None
     Require all granted
</Directory>


Virutal Host 파일 수정사항

<VirtualHost *:80>
    DocumentRoot "/usr/local/apache/htdocs"
    ServerName abc.com
    ServerAlias www.abc.com
</VirtualHost>

<VirtualHost *:8080>
    DocumentRoot "/home/httpd/abc/www/htdocs"
    ServerName abc.com
    ServerAlias www.abc.com
</VirtualHost>


블로그 이미지

Link2Me

,
728x90

본 자료는 인터넷 검색과 책을 약간 참조한 것이며, 어디까지나 참고삼아 보면 좋을거 같아 적어둔다.


 이마

 이마는 초년운(1~25세)를 본다.

 이마가 맑고 깨끗하고 흉이 없으면 어린 시절 유복하게 잘 자란다.

 이마에 흉터, 점, 사마귀 등이 있으면 어린 시절 힘들거나 방황을 많이 한다.

 여성이 남성적인 이마를 가지고 있다면 기가 쎄고 남자를 업신여긴다.

 이마가 매우 좁고 잔털까지 있어 이마의 경계가 없이 너저분하면 남자를 피곤하게 할 것이다.

 눈썹

 초승달 눈썹은 인정이 깊고 형제관계가 좋다.

 명궁

 눈썹과 눈썹 사이를 명궁이라 한다.

 명궁이 역삼각형에 맑고 깨끗하면 크게 성공한다.

 명궁이 지나치게 넓으면 바람끼가 있다.

 털이 나면 잘 뽑아주고 손질하고 이쁘게 가꾸면 운이 좋아질 수 있다.

 명궁 사이에 세로 주름이 있으면 신경이 날카롭고 남 갈구기를 잘한다.

 미간이 좁은 여성은 성기의 수축력이 좋은 명기의 소유자다.

 처접궁

 결혼운을 보는 곳이다.

 처접궁에 흉터가 있으면 결혼에 실패할 가능성이 많다.

 나이가 많지 않음에도 처접궁에 주름이 많다면 바람둥이 이거나 색골을 가망이 높다.

 맑고 깨끗한 처접궁을 가진 사람은 좋은 배우자가 될 수 있고 결혼생활도 행복할 것이다.

 누당

 눈 아래를 누당이라 하며, 자식복을 보는 곳이다.

 누당이 잘 발달한 여성은 선천적으로 생각이 밝고 쾌활하며 어두운 구석이 별로 없다.

 눈

 눈이 맑으며 눈동자의 검은 색이 짙으면 사업이 번창할 운이다.

 쌍커풀이 없는 여자는 거의 바람을 안피운다.
 검은 눈동자가 작아 흰자위가 많이 보이는 눈은 성격이 흉포하다.
 삼백안은 인생이 파란만장하고 말년에 고독하다.
 젖은 눈을 가진 여자는 정조관념이 없어서 이놈저놈에게 몸을 잘 준다고 한다.

 눈이 불룩 튀어 나온 사람은 성격이 급하고 인내력과 의지력이 부족하다.

 산근

 눈과 눈 사이를 산근이라 한다.
 산근은 배움, 학력, 지적수준을 보는 곳이다.

 코

 재백궁이라 하여 재물운을 보는 곳이다.

 콧대가 많이 휘면 인생이 되는게 하나도 없고 파란만장하다.

 콧대가 좀 휘었다면 성형을 하라. 성형해서 얼굴이 변하면 사람의 운도 변한다.

 코에 살이 없고 뾰족하면 재물을 모을 수가 없다.

 너무 큰 주먹코는 여자를 밝히고 주색에 능하다.

 매부리코나 콧구멍이 위로 들려 있으면 재물운이 적다.

 광대뼈

 남자가 광대뼈가 솟아 오르면 높은 관직에 오를 수 있다고 한다.

 여자가 광대뼈가 너무 높으면 과부상이라 한다.

 광대뼈가 높은 여자는 집에서 살림하면 안되고 직장생활을 해야 한다.

인중

 인중이 짧으면 단명상이다.
 남자가 좋은 인중을 가졌다면 정력도 좋고 자식도 많으며 장수한다.

 입술

 입끝이 아래로 처지면 운도 없고 되는 일도 하나도 없고 성격 또한 부정적이다.

 입을 다물었을 때 입의 양 끝이 아래로 처지면 좋지 않다.

 살짝 웃는 듯한 입 모양은 면접 볼 때도 많은 도움이 된다.
 입이 큰 사람은 대범하고 입이 작은 사람은 소심하다.
 입술이 두터운 사람은 애정도 깊고 정도 많다.
 윗입술과 아랫입술이 아주 얇은 사람은 매정하다. 여자는 과부상이다.

 아랫입술만 툭 튀어나온 여자는 과부상이다.

 턱

 

 귀

 귀가 깨끗하고 색이 좋으면 콩팥이 건강하다는 증거다.

 귀의 윤곽이 뚜렷하고 귓문이 넓으면 총명하다.

 귀가 뒤로 젖혀지거나 귓바퀴·귓불이 뒤로 뒤집혀 있으면 출세를 못 하고 가난하게 살 수 있다.

 작은 귀는 사업을 하지 말고 다른 사람 밑에서 일하는게 좋다.

 작은 귀는 감정적이며 성격이 급하지만, 영리하고 재주가 많다.

 작은 귀를 가진 여성은 남편 덕이 없다.

 

 왼손 : 선천적인 성격, 재능
 오른손 : 후천적인 노력으로 생긴 현재와 미래를 의미한다.


'알면 좋은 정보' 카테고리의 다른 글

행복소통 청원 지지 부탁드립니다  (0) 2020.07.28
청춘  (0) 2015.11.11
혈액형과 성격구분  (0) 2013.06.30
블로그 이미지

Link2Me

,
728x90

https://gist.github.com/joashp/a1ae9cb30fa533f4ad94 를 참조하여 AES 암호화, 복호화 알고리즘을 테스트 했다.


PHP에서 문자열을 암호화할 때 보다 더 강력하게 암호화할 수 있는 salt(솔트), hash 256를 사용한 AES 암호화, 복호화 알고리즘이다.

가장 중요한 것은 secret_key 를 관리하는 방법이다.

web 접속 경로상에 없도록 하거나, 다른 서버에 관리하는 것이 좋다.

아래 예제에서는 $salt 를 이용하여 생성한 값을 가지고 임의로 $secret_key 를 만들었다.


윈도우 기반 autoset9 에서는 openssl_encrypt 에러가 발생하여 리눅스 기반에서 테스트를 했다.


사용법 예제

<?php
include_once 'dbController.php';
require_once 'loginClass.php';
$c = new LoginClass();

$salt = sha1(mt_rand());
echo $salt.'<br />';
$secret_key = "f9a90bfa9e8ddd9965fecc0052e6786cf59f3131";
echo $secret_key.'<br />';
$secret_iv = substr($salt, 0, 20);
echo $secret_iv.'<br />';

$str = '안녕하세요? 홍길동입니다.';
$encrypted = $c->AES_encrypt($str);
echo $encrypted.'<br />';
echo 'encrypted message length : '.strlen($encrypted).'<br />';

$decrypted = $c->AES_decrypt($encrypted);
echo $decrypted.'<br />';
?>



Class

<?php
class DBController {
    private $host = 'localhost';
    private $database = 'test';
    private $userid = 'root';
    private $password = 'autoset';
    protected $db; // 변수를 선언한 클래스와 상속받은 클래스에서 참조할 수 있다.

    public function __construct() {
        $this->db = $this->connectDB();
        // construct 메소드는 객체가 생성(인스턴스화)될 때 자동으로 실행되는 특수한 메소드다.
    }

    function __destruct(){
        mysqli_close($this->connectDB());
        //mysqli_close($this->db);
    }

    private function connectDB() {
        $dbconn = mysqli_connect($this->host, $this->userid, $this->password, $this->database);
        mysqli_set_charset($dbconn, "utf8"); // DB설정이 잘못되어 euc-kr 로 되어 있으면 문제가 됨
        if (mysqli_connect_errno()) {
           printf("Connect failed: %s\n", mysqli_connect_error());
           exit();
        } else {
          return $dbconn;
        }
    }
}//end dbClass

?>

<?php
class LoginClass extends DBController {
    // class 자식클래스 extends 부모클래스
    // override : 부모 클래스와 자식 클래스가 같은 메소드를 정의했을 경우 자식 클래스가 우선시된다.
    // AES 암호화
    function AES_encrypt($plain_text){
        global $secret_key, $secret_iv;
        $encrypt_method = "AES-256-CBC";
        $key = hash('sha256', $secret_key);
        $iv = substr(hash('sha256', $secret_iv), 0, 16);
        $output = openssl_encrypt($plain_text, $encrypt_method, $key, true, $iv);
        return base64_encode($output);
    }

    // AES 복호화
    function AES_decrypt($encrypted_string){
        global $secret_key, $secret_iv;
        $encrypt_method = "AES-256-CBC";
        $key = hash('sha256', $secret_key);
        $iv = substr(hash('sha256', $secret_iv), 0, 16);
        $output = openssl_decrypt(base64_decode($encrypted_string), $encrypt_method, $key, true, $iv);
        return $output;
    }

}//end class LoginClass
?>


https://gist.github.com/kijin/8404004 를 참조하여 AES 암호화, 복호화를 해도 좋을 거 같다.


'Web 프로그램 > 암호화 처리' 카테고리의 다른 글

phpMyAdmin 해킹시도 흔적  (0) 2018.06.11
[PHP] SHA384 패스워드 만들기  (0) 2017.01.27
[보안] PHP AES 암호화 예제  (0) 2016.10.16
[보안] SEED PHP 사용법  (0) 2016.10.15
블로그 이미지

Link2Me

,