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 안에서 구현하면 된다.
'국비교육(22-23)' 카테고리의 다른 글
105일차(1)/Android App(70) : 모바일 갤러리 기능 구현(4) (0) | 2023.03.11 |
---|---|
104일차(1)/Android App(69) : 모바일 갤러리 기능 구현(3) (0) | 2023.03.09 |
102일차(1)/Android App(67) : 모바일 갤러리 기능 구현(1) (0) | 2023.03.08 |
101일차(1)/Android App(66) : 카메라 앱으로 사진 촬영, 저장(3) / 서버 전송 (0) | 2023.03.07 |
99일차(2)/Android App(65) : 카메라 앱으로 사진 촬영, 저장(2) (0) | 2023.03.04 |