101일차(1)/Android App(66) : 카메라 앱으로 사진 촬영, 저장(3) / 서버 전송
- 이전 예제 코드리뷰
- 서버로 찍은 사진 파일 전송하기
- 이전에 만든 카메라를 활용하는 앱 코드리뷰!
2023.03.03 - [국비교육] - 99일차(1)/Android App(64) : 카메라 앱으로 사진 촬영, 저장
- 터치 입력에 반응하는 확대,축소가 가능한 이미지뷰를 만들어봄
- 특정 패키지에 이 TouchImageView를 집어넣어 놓으면 쉽게 사용할 수 있다
- 특정 클래스를 사용하고 싶은 경우 패키지명.클래스명 으로 사용하면 된다.
- 이전에 슈팅게임을 만들때 GameView를 사용했던 것처럼!
- 기존 이미지뷰에 터치기능을 가미한 것
- 이곳에 출력하는 이미지는 카메라어플로 찍은 것이다.
- 이미지를 캡쳐하고자 하는 의도를 갖고 있는 intent 객체를 생성해서,
- startActivityForResult() 메소드에 전달하면 안드로이드 운영체제에서 이 액티비티를 찾아서 활성화시켜준다.
- startActivityForResult()는 결과가 있는 메소드이다.. 사진을 찍은 결과를 가져온다!
- 이전에 연습했던 전화를 거는 예제에서는, intent를 가지면 startActivity로 넣어주었다.
- 이렇게 메소드를 호출하면서 바로 intent 를 넣어주어도 되고,
아니면 의도를 생성자의 인자로 넣어주어도 사용 가능하다.(사진찍기)
- 단 이것은 전화를 거는 기능으로 끝난다! 어떤 결과를 받아오는 작업은 아니다.
- 2개의 메소드는 둘이 짝으로 있는 메소드이다
- 1이라는 req 코드가 들어오면 자동으로 onActivityResult 메소드가 호출된다.
- req code가 1이고 RESULT_OK가 들어오는 조건에서만 아래 코드가 수행되도록 한다!
- URI 안에 파일 객체를 담고 이 인텐트를 카메라앱에 전달한 개념이라고 보면 된다!
- 카메라 앱에서는 사진 데이터를 저 파일객체를 사용해서 저장한다.
- 출력할 String 객체를 이용해서 저장한다.
- 저 파일 객체는 어디에 만들어진 것인지?
- MyApp 안에 파일을 저장할 권한을 주기위해서 Provider로 만들었다.
- FileProvider.getUriForFile() 메소드를 활용해 권한을 준것이다.
- 저 "" 안의 내용은 Provider의 아이디라고 생각하면 된다.
- AndroidManifest에 Provider가 등록되어 있어야 한다.
- 단, 이 아이디는 이 안에서 유일한 것이여야 한다. 그래서 패키지명을 조합해서 쓰는것!
- filePath를 받아서 쓰는 것이다.
- 이 권한은 외부에서 우리 파일에 access 하기 위해서 필요하다.
- 이 권한의 아이디를 사용해서 MainActivity에서 불러다가 쓰는 것이다.
- putExtra() 로 intent에 담아서 전달. 출력용도로 써달라는 의미!
- 찍은 파일은 이 경로에 저장된다. (내부저장소)
- 촬영한 이미지 데이터가 이렇게 들어가있다.
- 여기에 파일을 만들수있는 파일객체를 하나 생성해서,
이 파일객체를 Uri에 포장하고, 그 Uri를 intent 로 포장해서 카메라앱에 전달하는 것이다.
- 겹치지않도록 UUID로 파일명을 생성하고, .jpg를 붙여주었다.
** 서버로 찍은 사진 파일 전송하기
- 이렇게 안드로이드에서 카메라 앱으로 사진을 찍어 저장한 파일에 추가로 어떤 작업을 할 수 있을까?
- 스프링부트 서버에 저장하기, 업로드된 정보들을 DB에 저장하기 등, 갤러리와 연동된 앱을 만들 수 있다!
- 저장된 사진에 대한 정보를 json 목록으로 출력해주면
안드로이드 app에서도 갤러리 목록을 보여주는 기능을 만들 수 있다.
- 파일을 업로드하려면 로그인이 필요하도록 설정
- 로그인 정보를 select해서 목록 출력하기
- 그럼 파일을 전송하는 방법은? get, post방식으로 전송할 수는 있지만 파일은 어떻게 전송할까?
- 웹브라우저라면 <form enctype="multipart/form-data"> <input type="file" .... > 이렇게 전송하면 웹브라우저가 알아서 해줬지만, 안드로이드는 FileData를 직접 실어서 보내야한다.
- 2진데이터, 파일명 등등... 부수적인 정보를 직접 보내야 한다.
- 파일데이터를 읽어서 인터넷으로 보내기!
- 모든 파일은 byte (2진수 8개) 알갱이로 구성되어 있다. 이미지파일도 마찬가지이다.
- xxx.jpg 파일이 있으면 이 파일도 byte 알갱이로 구성되어 있다.
- 이 파일을 서버에 전송하고 싶으면 xxx.jpg 파일에서 읽어들일 스트림 객체(InputStream or FileInputStream) 을 이용해서 byte 알갱이를 읽어들인 후, network(인터넷)을 통해서 스트림 객체(OutputStream)을 이용해서 출력한다.
- 반복문으로 읽어들인 후 네트워크를 사용해서 출력한다!
- 이미지와 같이 읽어들여서 네트워크를 통해서 출력!
- 스프링 부트 서버에서 받아서 서버의 파일시스템에 저장하고, DB에 저장한다.
- 안드로이드에서는 HttpUrlConnection 객체를 이용해서 참조값을 얻어낸다.
** 네트워크를 사용해 파일을 전송하는 메소드
- httpUrlConnection 객체 사용
- 요청 url만 잘 전달하면 된다.
- form에서 파일을 작성하던 방식으로 쓰면 위와 같다.
- connection에 이와 같은 설정을 해주는 것이다. muiltipart/form-data 라는것을 알려주기!
- boundary는 경계선이다. 경계선 설정에는 어떤 문자열을 활용할 예정
[ 서버에 출력하는 데이터 예시 ]
- 이런 문자열을 서버에 보낸다고 하면, 중간에 위와 같은 경계선을 만들 수 있다.
- 이런 경계선으로 데이터를 구분할 수 있다. (문자열 데이터/2진 데이터 등...)
- 이 경계선이 바운더리 설정이다!
- 이 바운더리를 아래에서 활용하고 있다.
- 파일 데이터만 보내는것이 아니고, 파일명, 파일 정보를 함께 보내서 쓰는 것
- 이것은 서버에게 추가정보를 보내는것이다.
- User Agent 가 Java Code라는 것을 알려주는 것
- connection으로 outputstream을 얻어낸다.
- 파일에서 읽어들여서 outputStream을 사용해서 출력한다.
- boundary를 사용해서 출력했음을 알려준다.
activity_main.xml
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<EditText
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:id="@+id/inputCaption"
android:hint="설명 입력..."/>
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="업로드"
android:id="@+id/uploadBtn"/>
</LinearLayout>
- 새 리니어레이아웃(horizontal)으로 버튼, EditText를 추가해주기
- 이 4개의 컴포넌트는 높이를 경쟁한다.(vertical)
- 그런데 지금 123으로 이미 꽉 차서, 높이를 다 갖게 되어있어서 4는 들어갈 자리가 없다.
- 이대로 run 해보면 EditText는 자리가 없어서 보이지 않는다.
- TouchImageView가 일단 높이를 가지고 있지 않다가(0dp) 남는 높이를 다 가지도록 수정해주면 된다.
- 그러면 이런 형태가 된다. EditText에 내용을 입력하고 버튼을 누르면 파일과 텍스트가 같이 전송되도록 할 것이다.
- 위와 같이 사진에 대한 설명을 넣고 서버에 전송하면 된다.
- input type="file", input type="text" 를 함께 전송한다고 생각하면 된다.
- step18login에서 로그인 기능 가져오기
//view binding 을 사용하기 위한 설정
buildFeatures {
viewBinding = true
}
- gradle 파일에 뷰바인딩 설정 추가 + Sync Now!!
- Activity 2개, xml 파일 2개 복사해오기
- login, logout에 필요한 파일에 AppConstants도 추가
- AppConstants는 BASE_URL 만 남겨두기
MainActivity
package com.example.step25imagecapture;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.content.FileProvider;
import android.content.ComponentName;
import android.content.Context;
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.TextView;
import android.widget.Toast;
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.io.Serializable;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLConnection;
import java.util.ArrayList;
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;
@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->{
//사진을 찍고 싶다는 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(this,
"com.example.step25imagecapture.fileprovider",
photoFile);
intent.putExtra(MediaStore.EXTRA_OUTPUT, uri);
startActivityForResult(intent, 1);
});
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);
}
}
}
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);
}
}
}
- onStart에서 로그인했는지 확인해서 하지 않았으면 LoginActivity로 보내는 절차
- Spring Boot에서 코드 수정
- MusicController 에서 일부 가져오기(로그인 메소드 등...)
GalleryController
//안드로이드 앱에서 사진을 업로드하는 요청을 처리하는 메소드
@PostMapping("/api/gallery/insert")
@ResponseBody
public Map<String, Object> apiInsert(GalleryDto dto, HttpServletRequest request){
/*
GalleryDto에 담긴 내용을 활용해서 이미지를 파일시스템에 저장하고 이미지 정보를 DB에도 저장한다.
*/
service.saveImage(dto, request);
Map<String, Object> map=new HashMap<>();
map.put("isSuccess", "true");
return map;
}
- GalleryController에 안드로이드용 메소드 추가
- 서비스에 이 업로드하는 로직이 이미 들어있다.
- Service의 saveImage 메소드를 사용한다.
- 파일시스템, DB에 모두 저장한다.
- GalleryDto를 보면 caption, image가 있다.
- 앱에서는 사진에 대한 설명과 파일을 전송한다.
- form 형태라면 이렇게 전달할 것! 필드명과 input 요소의 name속성의 value를 일치시킨다.
- 안드로이드에서도 이미지를 업로드할때 각각 저 caption 과 image 라는 파라미터로 정보를 직접 보내주어야 한다.
→ 안드로이드에서 caption, image 라는 단어를 기억하고 코딩해야 한다!
- config 로그인 설정 (인터셉터)
mobileLoginInterceptor
- "/api/gallery/insert" 경로 추가
- 갤러리 list는 무조건 요청이 가능하도록 할 예정(비회원 가능)
activity_login.xml 수정
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/inputId"
android:hint="아이디..."
android:layout_marginTop="100dp"/>
- 세로 가운데정렬을 없애고 marginTop만 추가!
MainActivity 수정
@Override
protected void onPostExecute(Boolean isLogin) {
super.onPostExecute(isLogin);
if(!isLogin){
Intent intent = new Intent(MainActivity.this, LoginActivity.class);
startActivity(intent);
}
}
- LoginCheckTask를 로그인되었는지 여부를 알아내서 onPostExecute로 리턴해준다.
- 새로 만든 버튼도 참조값 찾아주기!
- 파라미터 타입 map, 과정타입 없음(void), 결과타입 String 인 UploadTask 클래스 생성
- doInBackground, onPostExecute 를 오버라이드
- 위에서 봤던 파일 전송 메소드를 doInBackground 메소드 안에 작성해준다.
- 필드, 생성자 만들어주기
- 캐쉬가 만들어지지 않도록, boundary를 그때그때 다르게 하고자 currentTimeMillis() 사용
- URL 경로는 이렇게 AppConstants를 사용해서 넣어준다.
- getOutputStream() 으로 읽어온다.
- map에 담아서 전달하면 while문 안에서 돌면서 이 안에서 구성해준다.
- caption이라는 키값(파라미터명)으로 hello를 담아서 넘겨준다고 하면 여기서 map에 담아 저장해주는 것
- 파일 저장경로는 imagePath를 사용한다.
- MainActivity의 필드 imagePath를 filePath에 넣어주고, 파일명(fileParamName)도 같이 전송해준다.
- 파일에서 읽어들일 스트림 객체 얻어내기
- 위 내용을 이전에 작성했던 방식으로 변경했다.
- 미리 준비한 바이트 배열을 전달해서 파일로 읽어들이고, 읽은만큼 출력해준다.
- readedByte에서 -1이 리턴되면 루프를 빠져나오도록 한다.
//App 에 저장된 session id 가 있다면 요청할때 쿠키로 같이 보내기
if(!sessionId.equals("")) {
// JSESSIONID=xxx 형식의 문자열을 쿠키로 보내기
conn.setRequestProperty("Cookie", sessionId);
}
- 그리고 로그인정보도 같이 보내야한다. 복사해서 메소드안으로 넣어줌
- 서버가 응답한 쿠키 목록(cookList) 부분도 필요하므로 응답을 다 받은 다음에 추가하기
- 이 부분에 추가
- 해시맵은 사용하지 않으므로 지워주었다.
- onCreate 안에서 EditText의 참조값을 얻어내고, 이 uploadTask를 사용하는 코드를 추가한다.
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=".MainActivity"
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=".LoginActivity" android:exported="false"/>
<activity android:name=".LogoutActivity" android:exported="false"/>
<provider
android:authorities="com.example.step25imagecapture.fileprovider"
android:name="androidx.core.content.FileProvider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/filepaths"/>
</provider>
</application>
</manifest>
- 인터넷 사용 권한 추가
- 액티비티 2개도 추가
- 로그인하고 나서 사진 찍기 (원본 사진)
- 아래 EditText에서 문자열을 입력해서 전송한다.
- DB에서 확인해보면 파일이 업로드되어 있다.
- 웹브라우저의 갤러리 목록보기에서도 확인할 수 있다.
- 이후에는 안드로이드에서 보이는 화면을 리스트 뷰로 이미지가 작게 보이도록 처리하고, 클릭하면 크게 보이도록 수정할 예정!
(그러면 페이징 처리도 필요없고 스크롤로만 로딩하면 된다.)
'국비교육(22-23)' 카테고리의 다른 글
103일차(1)/Android App(68) : 모바일 갤러리 기능 구현(2) (0) | 2023.03.09 |
---|---|
102일차(1)/Android App(67) : 모바일 갤러리 기능 구현(1) (0) | 2023.03.08 |
99일차(2)/Android App(65) : 카메라 앱으로 사진 촬영, 저장(2) (0) | 2023.03.04 |
99일차(1)/Android App(64) : 카메라 앱으로 사진 촬영, 저장 (0) | 2023.03.03 |
98일차(1)/Android App(63) : Internal, External Storage에서 읽기 (read) (1) | 2023.03.02 |