국비교육(22-23)

103일차(1)/Android App(68) : 모바일 갤러리 기능 구현(2)

서리/Seori 2023. 3. 9. 00:52

103일차(1)/Android App(68) : 모바일 갤러리 기능 구현(2)

 

- 이전 게시물 참고

2023.03.08 - [국비교육] - 102일차(1)/Android App(67) : 모바일 갤러리 기능 구현(1)

 

 

- 아답타에 모델을 연결하고, 그 아답타는 리스트뷰에 연결했다.

- 모델이 갖고있는 데이터를 변경해서 아답타가 View를 보여준다.

 

- 데이터를 사용해서 뷰를 만들고 이것을 리스트뷰의 셀에 연결!

 

 

- GalleryAdapter의 getView 메소드는 리스트뷰를 리턴한다.

- 뷰 객체를 받아가서 셀 하나하나를 구성

- TextView는 직접 값을 넣어주면 되고, 이미지 출력은 의존디펜던시 Glide를 사용해서 해준다!

 

- 이곳에 출력할 데이터는 GalleryListActivity 에서 만들어주면 된다.

 

 

- Spring Boot에도 갤러리 목록을 응답해주는 컨트롤러 메소드를 만들어주었다.

- 안드로이드용으로 JSON으로 응답하는 컨트롤러 메소드를 생성

 

 

- WebConfig에 목록에는 로그인하지 않아도 되도록 설정!

 


* 모바일 화면 ListView 를 보여줄 메소드 구성하기

 

GalleryListActivity

package com.example.step25imagecapture;

import android.app.ProgressDialog;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.AsyncTask;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.util.Log;
import android.view.View;

import androidx.appcompat.app.AppCompatActivity;

import com.example.step25imagecapture.databinding.ActivityGalleryListBinding;

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

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;

public class GalleryListActivity extends AppCompatActivity implements View.OnClickListener{
    ActivityGalleryListBinding binding;
    //서버에서 받아온 갤러리 목록을 저장할 객체
    List<GalleryDto> list=new ArrayList<>();
    String sessionId;
    SharedPreferences pref;
    GalleryAdapter adapter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //바인딩 객체의 참조값을 필드에 저장
        binding=ActivityGalleryListBinding.inflate(getLayoutInflater());
        //바인딩 객체를 이용해서 화면 구성
        setContentView(binding.getRoot());

        //listView에 연결할 아답타 객체 생성
        adapter=new GalleryAdapter(this, R.layout.listview_cell, list);
        //ListView에 아답타 연결하기
        binding.listView.setAdapter(adapter);
        //버튼에 리스너 등록하기
        binding.takePicBtn.setOnClickListener(this);
        binding.refreshBtn.setOnClickListener(this);
        //ListView 의 cell을 클릭했을 때 리스너
        binding.listView.setOnItemClickListener((parent, view, position, id) -> {
            //position은 클릭한 cell의 인덱스 값이다.
            GalleryDto dto=list.get(position);
            Intent intent=new Intent(this, DetailActivity.class);
            startActivity(intent);
        });
    }

    @Override
    protected void onStart() {
        super.onStart();

        pref= PreferenceManager.getDefaultSharedPreferences(this);
        sessionId=pref.getString("sessionId", "");

        //원격지 서버로부터 갤러리 목록을 받아오는 요청을 한다.
        new GalleryListTask().execute(AppConstants.BASE_URL+"/api/gallery/list");
    }

    //버튼을 눌렀을때 호출되는 메소드
    @Override
    public void onClick(View v) {
        switch (v.getId()){
            case R.id.takePicBtn:
                //사진을 찍어서 올리는 액티비티를 실행한다.
                Intent intent=new Intent(this, MainActivity.class);
                startActivity(intent);
                break;
            case R.id.refreshBtn:
                //목록을 다시 받아온다.
                new GalleryListTask().execute(AppConstants.BASE_URL+"/api/gallery/list");
                break;
        }
    }

    //갤러리 목록을 얻어올 작업을 할 비동기 task
    class GalleryListTask extends AsyncTask<String, Void, String> {

        //진행중 알림을 띄우기 위한 객체
        ProgressDialog progress=new ProgressDialog(GalleryListActivity.this);

        @Override
        protected void onPreExecute() {
            super.onPreExecute();
            //진행중 알림을 띄운다.
            progress.setProgressStyle(ProgressDialog.STYLE_SPINNER);
            progress.setMessage("로딩중입니다...");
            progress.show();
        }

        @Override
        protected String doInBackground(String... strings) {
            //요청 url
            String requestUrl=strings[0];
            //서버가 http 요청에 대해서 응답하는 문자열을 누적할 객체
            StringBuilder builder=new StringBuilder();
            HttpURLConnection conn=null;
            InputStreamReader isr=null;
            BufferedReader br=null;
            try{
                //URL 객체 생성
                URL url=new URL(requestUrl);
                //HttpURLConnection 객체의 참조값 얻어오기
                conn=(HttpURLConnection)url.openConnection();
                if(conn!=null){//연결이 되었다면
                    conn.setConnectTimeout(20000); //응답을 기다리는 최대 대기 시간
                    conn.setRequestMethod("GET");//Default 설정
                    conn.setUseCaches(false);//케쉬 사용 여부
                    //App 에 저장된 session id 가 있다면 요청할때 쿠키로 같이 보내기
                    if(!sessionId.equals("")) {
                        // JSESSIONID=xxx 형식의 문자열을 쿠키로 보내기
                        conn.setRequestProperty("Cookie", sessionId);
                    }

                    //응답 코드를 읽어온다.
                    int responseCode=conn.getResponseCode();

                    if(responseCode==200){//정상 응답이라면...
                        //서버가 출력하는 문자열을 읽어오기 위한 객체
                        isr=new InputStreamReader(conn.getInputStream());
                        br=new BufferedReader(isr);
                        //반복문 돌면서 읽어오기
                        while(true){
                            //한줄씩 읽어들인다.
                            String line=br.readLine();
                            //더이상 읽어올 문자열이 없으면 반복문 탈출
                            if(line==null)break;
                            //읽어온 문자열 누적 시키기
                            builder.append(line);
                        }
                    }
                }
                //서버가 응답한 쿠키 목록을 읽어온다.
                List<String> cookList=conn.getHeaderFields().get("Set-Cookie");
                //만일 쿠키가 존대 한다면
                if(cookList != null){
                    //반복문 돌면서
                    for(String tmp : cookList){
                        //session id 가 들어 있는 쿠키를 찾아내서
                        if(tmp.contains("JSESSIONID")){
                            //session id 만 추출해서
                            String sessionId=tmp.split(";")[0];
                            //SharedPreferences 을 편집할수 있는 객체를 활용해서
                            SharedPreferences.Editor editor=pref.edit();
                            //sessionId 라는 키값으로 session id 값을 저장한다.
                            editor.putString("sessionId", sessionId);
                            editor.apply();//apply() 는 비동기로 저장하기 때문에 실행의 흐름이 잡혀 있지 않다(지연이 없음)
                            //필드에도 담아둔다.
                            GalleryListActivity.this.sessionId=sessionId;
                        }
                    }
                }

            }catch(Exception e){//예외가 발생하면
                Log.e("MusicListTask", e.getMessage());
            }finally {
                try{
                    if(isr!=null)isr.close();
                    if(br!=null)br.close();
                    if(conn!=null)conn.disconnect();
                }catch(Exception e){}
            }
            //응답받은 문자열을 리턴한다.
            return builder.toString();
        }

        @Override
        protected void onPostExecute(String jsonStr) {
            super.onPostExecute(jsonStr);
            //여기는 UI 스레드 (자유롭게 UI작업을 할수있다)
            //jsonStr 은 [{},{},...] 형식의 문자열이기 때문에 JSONArray 객체를 생성한다.
            list.clear();
            try {
                JSONArray arr=new JSONArray(jsonStr);
                for(int i=0; i<arr.length(); i++){
                    //i번째 JSONObject 객체를 참조
                    JSONObject tmp=arr.getJSONObject(i);
                    int num=tmp.getInt("num");
                    String writer=tmp.getString("writer");
                    String caption=tmp.getString("caption");
                    String imagePath=tmp.getString("imagePath");
                    String regdate=tmp.getString("regdate");
                    GalleryDto dto=new GalleryDto();
                    dto.setNum(num);
                    dto.setWriter(writer);
                    dto.setCaption(caption);
                    //http://xxx/xxx/resources/upload/xxx.jpg 형식의 문자열을 구성해서 넣기
                    dto.setImagePath(AppConstants.BASE_URL+imagePath);
                    dto.setRegdate(regdate);
                    //ArrayList 객체에 누적시키기
                    list.add(dto);
                }
                //모델의 데이터가 바뀌었다고 아답타에 알려서 listView가 업데이트 되도록 한다.
                adapter.notifyDataSetChanged();

            } catch (JSONException je) {
                Log.e("onPostExecute()", je.getMessage());
            }
            //프로그레스 취소
            progress.dismiss();
        }
    }
}

 

- 이전에 만든 mp3 예제에서 MusicListTask 메소드를 복사해왔다.

 

 

- onStart에서 추가. pref로 로그인 아이디 받아오기!

 

 

- 반복문에서 정보를 읽어와서 onPostExecute 에서 json문자열로 리턴한다.

 

 

- 갤러리 목록의 dto에 맞게 수정, 경로 지정

- DTO에 값을 담아주면서 이미지 list에 넣는다. list 모델에 누적시킨 후 아답타에 알리기!

 

 

- 아답타를 아래 GalleryListTask 메소드에서 사용해야 하므로 필드로 만들어주었다.

 

 

- onStart 안에서 메소드를 사용해 요청한다.

- 그러면 이 list가 GalleryListTask 메소드 안으로 들어간다!

 


 

AndroidManifest.xml

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

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

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/Theme.MyAndroid"
        android:usesCleartextTraffic="true">
        <activity
            android:name=".DetailActivity"
            android:exported="false" />
        <activity
            android:name=".GalleryListActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <activity
            android:name=".MainActivity"
            android:exported="false" />
        <activity
            android:name=".LoginActivity"
            android:exported="false" />
        <activity
            android:name=".LogoutActivity"
            android:exported="false" />

        <provider
            android:name="androidx.core.content.FileProvider"
            android:authorities="com.example.step25imagecapture.fileprovider"
            android:exported="false"
            android:grantUriPermissions="true">
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/filepaths" />
        </provider>
    </application>

</manifest>

 

 

- GalleyListActivity를 가장 첫 화면으로 바꾸기!

- <intent-filter>를 활성화시킬 액티비티 안에 넣어준다.

- android:exported 는 안드로이드 운영체제가 이 액티비티를 직접 활성화시킨다는 의미이다.

 

 

- 위와 같이 이동시켜주고, 나머지 Activity들은 전부 exported="false" 로 지정하기.

 

 

- 그러면 이렇게 안드로이드에서 이미지가 Glide를 사용해서 출력된다.

- 스크롤을 내릴 때마다 새로운 셀이 로딩된다.

 

 

- 웹 화면과 모바일 GalleryList 화면 비교!

(이전에 다른 예제에서 업로드한 데이터는 현재 경로가 맞지 않아서 보이지 않음)

 


 

* 갤러리 세부 기능 추가하기!

- 새로고침 버튼 클릭시 목록 다시 받아오기

- 로그인한 회원이 사진찍기 클릭시 바로 카메라 앱으로 이동시키기

- 클릭하면 사진이 크게 보이도록 작업 (detail)

 

 

- GalleryListActivity에서 버튼 클릭시의 작업 추가!

 

 

- '사진찍기' 버튼을 누르면 로그인 액티비티로 이동 - 로그인 후 사진을 찍으면

 

 

- 갤러리가 새로고침되며 목록에 찍은 사진이 새로 추가된다.

 


 

 

 

- 알림의 확인버튼 클릭시 다른 곳으로 이동하게 하고 싶다면,

 확인 버튼의 리스너에 람다식으로 작업을 추가한다!

 (람다식은 메소드가 1개일 때에만 쓸 수 있다)

 

 

- 이 액티비티를 끝낸다는 의미로 MainActivity.this 에 finish() 메소드 사용

 

 

- 이제 사진 업로드 후 확인버튼을 클릭하면 자동으로 (새로고침된) 액티비티로 이동한다.

 (MainActivity가 종료되었기 때문에 이전 액티비티로 돌아간 것)

 


 

- 갤러리를 불러오는 Async 작업이 시간이 오래 걸릴 수 있으므로.. 로딩 과정을 넣어줄 것

 

 

- 그런데 사용자 입장에서는 앱이 멈춘 건지 진행되고 있는 건지 알 수 없으므로... 로딩 기능을 넣어준다!

- ProgressDialog 객체에 GalleryActivity의 참조값을 넣어준다.

  (그냥 this 하면 AsyncTask를 가리키는 것이 된다.)

 

 

 

onPreExecute()

- Async가 실행되기 직전에 실행되는 메소드

- STYLE_SPINNER 돌아가는(회전) 진행 바를 추가!

- 일반 ProgressBar는 Horizontal 수평 바로 진행률을 보여주는데, 여기서는 작업의 진행률은 알 수 없으므로 회전으로 로딩한다는 것을 보여주기로 했다.

 

 

- show() 까지 해주어야 한다.

 

 

- 작업이 끝나고 나서 할 일은 onPostExecute 의 마지막에 한다.

- 원형으로 회전하는 ProgressBar를 취소해준다. (로딩 작업이 완료되었으므로)

 

 

@Override
protected void onPreExecute() {
    super.onPreExecute();
    //진행중 알림을 띄운다.
    progress.setProgressStyle(ProgressDialog.STYLE_SPINNER);
    progress.setMessage("로딩중입니다...");
    progress.show();
}

 

- 위와 같이 progress의 옵션을 작성해주면 로딩바에 메세지까지 나온다.

 


 

- listView_cell 좀더 가독성 좋게 수정

<?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="200dp">
    <ImageView
        android:layout_width="180dp"
        android:layout_height="match_parent"
        android:layout_margin="10dp"
        android:id="@+id/imageView"/>
    <LinearLayout
        android:layout_width="0dp"
        android:layout_weight="1"
        android:layout_height="match_parent"
        android:layout_margin="10dp"
        android:orientation="vertical"
        android:gravity="bottom">
        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:textSize="20sp"
            android:text="작성자:admin"
            android:id="@+id/writer"
            android:textStyle="bold"/>
        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:textSize="18sp"
            android:text="어쩌구 저쩌구..."
            android:id="@+id/caption"
            android:textStyle="italic"
            android:maxLines="1"
            android:ellipsize="end"/>
        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:textSize="18sp"
            android:text="2023.03.07 13:00"
            android:id="@+id/regdate"
            android:textStyle="bold"/>
    </LinearLayout>
</LinearLayout>

 

 

android:maxLines="1"
android:ellipsize="end"

- maxLines 최대 보여주는 길이는 1줄로 하고, 1줄이 넘어가면 ... 으로 표시되도록 함!

 

 

- 긴 캡션은 ... 으로 뒷부분이 생략된다.

 


 

- 로그인만 되어있다면 사진찍기를 클릭하지 않아도 바로 카메라가 열리도록 수정!

 

MainActivity

package com.example.step25imagecapture;

import android.content.Intent;
import android.content.SharedPreferences;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Matrix;
import android.media.ExifInterface;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.provider.MediaStore;
import android.util.Log;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.Toast;

import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.content.FileProvider;

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

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLConnection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;

public class MainActivity extends AppCompatActivity {
    ImageView imageView;
    //저장된 이미지의 전체 경로
    String imagePath;
    //필요한 필드
    String sessionId, id;
    SharedPreferences pref;
    //최초 사진을 찍었는지 여부
    boolean isTakePictured=false;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        //사진을 출력할 ImageView 의 참조값 필드에 저장하기
        imageView=findViewById(R.id.imageView);

        Button takePicture=findViewById(R.id.takePicture);
        takePicture.setOnClickListener(v->{
            //사진을 찍고 싶다는 Intent 객체 작성하기
            Intent intent=new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
            //운영체제에 해당 인턴트를 처리할수 있는 App 을 실행시켜 달라고 하고 결과 값도 받아올수 있도록 한다.
            startActivityForResult(intent, 0);

        });

        Button takePicutre2=findViewById(R.id.takePicture2);
        takePicutre2.setOnClickListener(v->{

        });

        EditText inputCaption=findViewById(R.id.inputCaption);

        //업로드 버튼에 대한 동작
        Button uploadBtn=findViewById(R.id.uploadBtn);
        uploadBtn.setOnClickListener(v->{
            //입력한 caption 과 찍은 사진 파일을 서버에 업로드 한다.
            String caption=inputCaption.getText().toString();
            //서버에 전송할 요철 파라미터를 Map 에 담고
            Map<String, String> map=new HashMap<>();
            map.put("caption", caption);
            //비동기 테스크를 이용해서 전송한다.
            new UploadTask().execute(map);
        });
    }

    @Override
    protected void onStart() {
        super.onStart();

        pref= PreferenceManager.getDefaultSharedPreferences(this);
        sessionId=pref.getString("sessionId", "");

        //로그인 했는지 체크하기 (로그인되지 않았을 경우에만)
        new LoginCheckTask().execute(AppConstants.BASE_URL + "/music/logincheck");

    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        //만일 위에서 요청한 요청과 같고 결과가 성공적이라면
        if(requestCode == 0 && resultCode == RESULT_OK){
            //data Intent 객체에 결과값(섬네일 이미지 데이터) 가 들어 있다.
            Bitmap image=(Bitmap)data.getExtras().get("data");
            //ImageView 에 출력하기
            imageView.setImageBitmap(image);
        }else if(requestCode == 1 && resultCode == RESULT_OK){
            //만일 여기가 실행된다면 imagePath 경로에 이미지 파일이 성공적으로 만들어진 것이다
            //Bitmap image=BitmapFactory.decodeFile(imagePath);
            //imageView.setImageBitmap(image);

            fitToImageView(imageView, imagePath);
        }
    }

    //이미지 뷰의 크기에 맞게 이미지를 출력하는 메소드
    public static void fitToImageView(ImageView imageView, String absolutePath){
        //출력할 이미지 뷰의 크기를 얻어온다.
        int targetW = imageView.getWidth();
        int targetH = imageView.getHeight();
        // Get the dimensions of the bitmap
        BitmapFactory.Options bmOptions = new BitmapFactory.Options();
        bmOptions.inJustDecodeBounds = true;
        BitmapFactory.decodeFile(absolutePath, bmOptions);
        int photoW = bmOptions.outWidth;
        int photoH = bmOptions.outHeight;

        // Determine how much to scale down the image
        int scaleFactor = Math.min(photoW/targetW, photoH/targetH);

        // Decode the image file into a Bitmap sized to fill the View
        bmOptions.inJustDecodeBounds = false;
        bmOptions.inSampleSize = scaleFactor;
        bmOptions.inPurgeable = true;
        Bitmap bitmap = BitmapFactory.decodeFile(absolutePath, bmOptions);
        /* 사진이 세로로 촬영했을때 회전하지 않도록 */
        try {
            ExifInterface ei = new ExifInterface(absolutePath);
            int orientation = ei.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_UNDEFINED);
            switch(orientation) {
                case ExifInterface.ORIENTATION_ROTATE_90:
                    bitmap = rotateImage(bitmap, 90);
                    break;
                case ExifInterface.ORIENTATION_ROTATE_180:
                    bitmap = rotateImage(bitmap, 180);
                    break;
                // etc.
            }
        }catch(IOException ie){
            Log.e("####", ie.getMessage());
        }

        imageView.setImageBitmap(bitmap);
    }
    //Bitmap 이미지 회전시켜서 리턴하는 메소드
    public static Bitmap rotateImage(Bitmap source, float angle) {
        Bitmap retVal;

        Matrix matrix = new Matrix();
        matrix.postRotate(angle);
        retVal = Bitmap.createBitmap(source, 0, 0, source.getWidth(), source.getHeight(), matrix, true);

        return retVal;
    }
    //로그인 여부를 체크하는 작업을 할 비동기 task
    class LoginCheckTask extends AsyncTask<String, Void, Boolean> {

        @Override
        protected Boolean doInBackground(String... strings) {
            //로그인 체크 url
            String requestUrl=strings[0];
            //서버가 http 요청에 대해서 응답하는 문자열을 누적할 객체
            StringBuilder builder=new StringBuilder();
            HttpURLConnection conn=null;
            InputStreamReader isr=null;
            BufferedReader br=null;
            boolean isLogin=false;
            try{
                //URL 객체 생성
                URL url=new URL(requestUrl);
                //HttpURLConnection 객체의 참조값 얻어오기
                conn=(HttpURLConnection)url.openConnection();
                if(conn!=null){//연결이 되었다면
                    conn.setConnectTimeout(20000); //응답을 기다리는 최대 대기 시간
                    conn.setRequestMethod("GET");//Default 설정
                    conn.setUseCaches(false);//케쉬 사용 여부
                    //App 에 저장된 session id 가 있다면 요청할때 쿠키로 같이 보내기
                    if(!sessionId.equals("")) {
                        // JSESSIONID=xxx 형식의 문자열을 쿠키로 보내기
                        conn.setRequestProperty("Cookie", sessionId);
                    }

                    //응답 코드를 읽어온다.
                    int responseCode=conn.getResponseCode();

                    if(responseCode==200){//정상 응답이라면...
                        //서버가 출력하는 문자열을 읽어오기 위한 객체
                        isr=new InputStreamReader(conn.getInputStream());
                        br=new BufferedReader(isr);
                        //반복문 돌면서 읽어오기
                        while(true){
                            //한줄씩 읽어들인다.
                            String line=br.readLine();
                            //더이상 읽어올 문자열이 없으면 반복문 탈출
                            if(line==null)break;
                            //읽어온 문자열 누적 시키기
                            builder.append(line);
                        }
                    }
                }
                //서버가 응답한 쿠키 목록을 읽어온다.
                List<String> cookList=conn.getHeaderFields().get("Set-Cookie");
                //만일 쿠키가 존대 한다면
                if(cookList != null){
                    //반복문 돌면서
                    for(String tmp : cookList){
                        //session id 가 들어 있는 쿠키를 찾아내서
                        if(tmp.contains("JSESSIONID")){
                            //session id 만 추출해서
                            String sessionId=tmp.split(";")[0];
                            //SharedPreferences 을 편집할수 있는 객체를 활용해서
                            SharedPreferences.Editor editor=pref.edit();
                            //sessionId 라는 키값으로 session id 값을 저장한다.
                            editor.putString("sessionId", sessionId);
                            editor.apply();//apply() 는 비동기로 저장하기 때문에 실행의 흐름이 잡혀 있지 않다(지연이 없음)
                            //필드에도 담아둔다.
                            MainActivity.this.sessionId=sessionId;
                        }
                    }
                }
                //출력받은 문자열 전체 얻어내기
                JSONObject obj=new JSONObject(builder.toString());
                /*
                    {"isLogin":false} or {"isLogin":true, "id":"kimgura"}
                    서버에서 위와 같은 형식의 json 문자열을 응답할 예정이다.
                 */
                Log.d("서버가 응답한 문자열", builder.toString());
                //로그인 여부를 읽어와서
                isLogin=obj.getBoolean("isLogin");
                //만일 로그인을 했다면
                if(isLogin){
                    //필드에 로그인된 아이디를 담아둔다.
                    id=obj.getString("id");
                }
            }catch(Exception e){//예외가 발생하면
                Log.e("LoginCheckTask", e.getMessage());
            }finally {
                try{
                    if(isr!=null)isr.close();
                    if(br!=null)br.close();
                    if(conn!=null)conn.disconnect();
                }catch(Exception e){}
            }
            //로그인 여부를 리턴하면 아래의 onPostExecute() 메소드에 전달된다.
            return isLogin;
        }

        @Override
        protected void onPostExecute(Boolean isLogin) {
            super.onPostExecute(isLogin);


            //만일 로그인 하지 않았다면
            if(!isLogin){
                //로그인 액티비티로 이동
                Intent intent=new Intent(MainActivity.this, LoginActivity.class);
                startActivity(intent);
            }else if(isLogin && !isTakePictured){ //만일 로그인을 했고 아직 사진을 찍은 상태가 아니라면
                //사진을 찍고 싶다는 Intent 객체 작성하기
                Intent intent=new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
                //외부 저장 장치의 절대 경로
                String absolutePath=getExternalFilesDir(null).getAbsolutePath();
                //파일명 구성
                String fileName= UUID.randomUUID().toString()+".jpg";
                //생성할 이미지의 전체 경로
                imagePath=absolutePath+"/"+fileName;
                //이미지 파일을 저장할 File 객체
                File photoFile=new File(imagePath);
                //File 객체를 Uri 로 포장을 한다.
                //Uri uri= Uri.fromFile(photoFile);
                Uri uri= FileProvider.getUriForFile(MainActivity.this,
                        "com.example.step25imagecapture.fileprovider",
                        photoFile);
                intent.putExtra(MediaStore.EXTRA_OUTPUT, uri);
                startActivityForResult(intent, 1);
                //사진을 이미 찍었다고 표시한다.
                isTakePictured=true;
            }
        }
    }

    class UploadTask extends AsyncTask<Map<String, String>, Void, String>{
        private String filePath;
        private String fileParamName;
        private final String boundary;
        private static final String LINE_FEED = "\r\n"; //개행기호 설정
        private String charset;

        //생성자
        public UploadTask(){
            // 경계선은 사용할때 마다 다른 값을 사용하도록 time milli 를 조합해서 사용한다. (캐쉬방지)
            boundary = "===" + System.currentTimeMillis() + "===";
            charset="utf-8";
            //서버에서 GalleryDto 의 image 라는 필드명과 일치(MultipartFile)
            fileParamName="image"; // <input type="file" name="image"/>
            //업로드할 파일이 어디 있는지 정보를 필드에 넣어준다.
            filePath=imagePath;
        }


        @Override
        protected String doInBackground(Map<String, String>... maps) {
            Map<String, String> param=maps[0];
            //서버가 http 요청에 대해서 응답하는 문자열을 누적할 객체
            StringBuilder builder=new StringBuilder();
            HttpURLConnection conn=null;
            InputStreamReader isr=null;
            PrintWriter pw=null;
            OutputStream os=null;
            FileInputStream fis=null;
            BufferedReader br=null;

            try{
                //URL 객체 생성
                URL url=new URL(AppConstants.BASE_URL+"/api/gallery/insert");
                //HttpURLConnection 객체의 참조값 얻어오기
                conn=(HttpURLConnection)url.openConnection();
                if(conn!=null){//연결이 되었다면
                    conn.setConnectTimeout(20000); //응답을 기다리는 최대 대기 시간
                    conn.setDoOutput(true);
                    conn.setDoInput(true);
                    conn.setRequestMethod("POST");
                    conn.setUseCaches(false);//케쉬 사용 여부

                    //App 에 저장된 session id 가 있다면 요청할때 쿠키로 같이 보내기
                    if(!sessionId.equals("")) {
                        // JSESSIONID=xxx 형식의 문자열을 쿠키로 보내기
                        conn.setRequestProperty("Cookie", sessionId);
                    }

                    //전송하는 데이터에 맞게 값 설정하기
                    conn.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + boundary);
                    conn.setRequestProperty("User-Agent", "CodeJava Agent");
                    //인터냇을 통해서 서버로 출력할수 있는 스트림 객체의 참조값 얻어오기
                    os=conn.getOutputStream();
                    //출력할 스트림 객체 얻어오기
                    pw=new PrintWriter(new OutputStreamWriter(os, charset));
                    //-------------- 전송 파라미터 추가  ------------------
                    if(param!=null){//요청 파리미터가 존재 한다면

                        Set<String> keySet=param.keySet();
                        Iterator<String> it=keySet.iterator();

                        //반복문 돌면서 map 에 담긴 모든 요소를 전송할수 있도록 구성한다.
                        while(it.hasNext()){
                            String key=it.next();
                            pw.append("--" + boundary).append(LINE_FEED);
                            pw.append("Content-Disposition: form-data; name=\"" + key + "\"")
                                    .append(LINE_FEED);
                            pw.append("Content-Type: text/plain; charset=" + charset).append(
                                    LINE_FEED);
                            pw.append(LINE_FEED);
                            pw.append(param.get(key)).append(LINE_FEED);
                            pw.flush();
                        }
                    }
                    //------------- File Field ------------------
                    File file=new File(filePath);
                    String filename=file.getName(); //파일명
                    pw.append("--" + boundary).append(LINE_FEED);
                    pw.append("Content-Disposition: form-data; name=\"" + fileParamName + "\"; filename=\"" + filename + "\"")
                            .append(LINE_FEED);
                    pw.append("Content-Type: " + URLConnection.guessContentTypeFromName(filename))
                            .append(LINE_FEED);
                    pw.append("Content-Transfer-Encoding: binary").append(LINE_FEED);
                    pw.append(LINE_FEED);
                    pw.flush();
                    //파일에서 읽어들일 스트림 객체 얻어내기
                    fis = new FileInputStream(file);
                    //byte 알갱이를 읽어들일 byte[] 객체 (한번에 4 kilo byte 씩 읽을수 있다)
                    byte[] buffer = new byte[4096];

                    //반복문 돌면서
                    while(true){
                        //byte 를 읽어들이고 몇 byte 를 읽었는지 리턴 받는다.
                        int readedByte=fis.read(buffer);
                        //더이상 읽을게 없다면 반복문 탈출
                        if(readedByte==-1)break;
                        //읽은 만큼큼 출력하기
                        os.write(buffer, 0, readedByte);
                        os.flush();
                    }

                    pw.append(LINE_FEED);
                    pw.flush();
                    pw.append(LINE_FEED).flush();
                    pw.append("--" + boundary + "--").append(LINE_FEED);
                    pw.flush();
                    //응답 코드를 읽어온다.
                    int responseCode=conn.getResponseCode();

                    if(responseCode==200){//정상 응답이라면...
                        //서버가 출력하는 문자열을 읽어오기 위한 객체
                        isr=new InputStreamReader(conn.getInputStream());
                        br=new BufferedReader(isr);
                        //반복문 돌면서 읽어오기
                        while(true){
                            //한줄씩 읽어들인다.
                            String line=br.readLine();
                            //더이상 읽어올 문자열이 없으면 반복문 탈출
                            if(line==null)break;
                            //읽어온 문자열 누적 시키기
                            builder.append(line);
                        }
                    }
                    //서버가 응답한 쿠키 목록을 읽어온다.
                    List<String> cookList=conn.getHeaderFields().get("Set-Cookie");
                    //만일 쿠키가 존대 한다면
                    if(cookList != null){
                        //반복문 돌면서
                        for(String tmp : cookList){
                            //session id 가 들어 있는 쿠키를 찾아내서
                            if(tmp.contains("JSESSIONID")){
                                //session id 만 추출해서
                                String sessionId=tmp.split(";")[0];
                                //SharedPreferences 을 편집할수 있는 객체를 활용해서
                                SharedPreferences.Editor editor=pref.edit();
                                //sessionId 라는 키값으로 session id 값을 저장한다.
                                editor.putString("sessionId", sessionId);
                                editor.apply();//apply() 는 비동기로 저장하기 때문에 실행의 흐름이 잡혀 있지 않다(지연이 없음)
                                //필드에도 담아둔다.
                                MainActivity.this.sessionId=sessionId;
                            }
                        }
                    }
                }
            }catch(Exception e){//예외가 발생하면
                Log.e("UploadTask", e.getMessage());
            }finally {
                try{
                    if(pw!=null)pw.close();
                    if(isr!=null)isr.close();
                    if(br!=null)br.close();
                    if(fis!=null) isr.close();
                    if(os!=null)os.close();
                    if(conn!=null)conn.disconnect();
                }catch(Exception e){}
            }
            //응답 받은 json 문자열 리턴하기
            return builder.toString();
        }

        @Override
        protected void onPostExecute(String s) {
            super.onPostExecute(s);
            //s는 {"isSuccess":true} or {"isSuccess":false} 형식의 문자열이다.
            try{
                JSONObject obj = new JSONObject(s);
                boolean isSuccess=obj.getBoolean("isSuccess");
                if(isSuccess){
                    new AlertDialog.Builder(MainActivity.this)
                            .setTitle("알림")
                            .setMessage("업로드했습니다.")
                            .setNeutralButton("확인",(dialog, which) -> {
                                //액티비티를 종료시켜서 GalleryListActivity가 다시 활성화되도록 한다.
                                MainActivity.this.finish();
                            })
                            .create()
                            .show();
                }else{
                    Toast.makeText(MainActivity.this, "실패했습니다.", Toast.LENGTH_SHORT).show();
                }
            }catch (JSONException je){
                Log.e("UploadTask", je.getMessage());
                Toast.makeText(MainActivity.this, "json 문자열이 아닙니다.", Toast.LENGTH_SHORT).show();
            }
        }
    }
}

 

 

- 로그인하지 않았으면 로그인액티비티로 이동, 

  로그인했으면 바로 이 사진을 촬영하는 작업이 실행되도록!

 

 

- if~else문으로 나누어 위 내용을 넣고 this만 수정해주면 된다.

 

 

- 그리고 onStart 메소드 안에서도 지금 로그인체크를 하게 되어있는데, 이것을 조건부로 변경한다.

 

 

- 이 코드가 최초에 한번만 호출되도록 하려면 어떻게 해야할까?

 

 

- 사진을 찍은 적이 있는지 여부를 필드로 관리한다. (초기값은 false)

 

 

 

- 사진을 찍었는지 여부를 이렇게 else if 문의 코드의 조건으로 넣어주고, 

  마지막에 isTakePictured 필드의 값을 true로 바꿔준다.

 

- Activity가 destroy된 상태에서 재실행되면 필드값은 초기화된다.

- 이제 이전에 사진을 찍은 정보가 있으면 카메라앱이 자동으로 실행된다.

 


 

- 클릭시 자세히보기로 이동하는 기능 추가

 

- 새 액티비티 생성

DetailActivity (아직 미완성)

package com.example.step25imagecapture;

import android.content.Intent;
import android.os.Bundle;

import androidx.appcompat.app.AppCompatActivity;

public class DetailActivity extends AppCompatActivity {

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

        //DetailActivity가 활성화될 때 전달받은 Intent 객체의 참조값 얻어오기
        //GalleryListActivity 에서 생성한 Intent 객체이기 때문에 "dto" 라는 키값으로 GalleryDto 객체가 들어있다.
        Intent intent=getIntent();
        GalleryDto dto=(GalleryDto)intent.getSerializableExtra("dto");
    }
}

 

- ListView 하나를 클릭했을 때 사진을 크게 볼 수 있는 화면(DetailActivity)으로 이동하기

 

 

- listView의 setOnItemClickListener를 람다식으로 작성해준다.

 

 

- view, index(position)과 id 값이 전달된다.

 

 

- GalleryDTO를 list로부터 얻어낸다.

- 위와 같이 람다식으로 쓰면 this가 밖으로 빠져나가진다.

  MainActivity.this 라고 할 필요가 없다.(밖에 있는 액티비티를 참조할 수 있다)

 

 

- 이 GalleryDto 객체가 DetailActivity 에 나타나는 것이다.

 

 

- 그러나 intent에 추가적인 정보를 전달하기 위해 putExtra()를 쓰려고 하는데,

 이 메소드에서 받아줄 GalleryDTO 타입이 없다.

 

 

- GalleryDto를 Serializable타입으로 구현해주기

 

 

- GalleryDto가 이렇게 여러 종류의 타입이 될 수 있다 (다형성)

- 이렇게 Serializable 타입이 필요하면 구현해서 사용하면 된다.

 

- 사실상 java의 대부분의 타입은 Serializable 이다. (빈 인터페이스이다)

 String , ArrayList, Map ... 모두 Serializable 타입이다.

 

 

- 그럼 이제 putExtra() 에서 GalleryDto를 받을 수 있다.

- DetailActivity에서 이것을 getIntent( ) 로 받아주면 된다.

 

 

- MainAcitivity에서 전달한 intent 를 받을 수 있는 메소드이다.

 

 

- dto라는 키값으로 Serializable 을 담아준 것이다.

- getSerializableExtra() 메소드로 불러온다. 하지만 사실은 GalleryDto 타입이므로 캐스팅해서 받아준다!

 

- 이미지 자세히 보기 기능은 이 DetailActivity 안에서 구현하면 된다.