공부일지

231219 (Android)

CD가참둥그렇다 2023. 12. 20. 09:52

 

소셜 로그인 기능 사용하기(네이버)

1. 네이버 로그인 API를 신청한다.

2. 개발환경설정

  • 네이버 로그인 API를 안드로이드 프로젝트 디펜던시에 그래들로 추가한다.

3. 네이버 아이디 로그인 객체를 초기화한다.

  • 자바에서는 INSTANCE과정이 추가된다.
NaverIdLoginSDK.INSTANCE.initialize(this,"클라이언트 Id","클라이언트 pw","클라이언트 이름");

4. 네이버 로그인 객체를 xml에 생성해준다.

<com.navercorp.nid.oauth.view.NidOAuthLoginButton 
	android:id="@+id/buttonOAuthLoginImg" 
    android:layout_width="wrap_content" 
    android:layout_height="50dp" />

5. 자바 파일에서 로그인 콜백 처리를 만들어준다.

binding.buttonOAuthLoginImg.setOAuthLogin(new OAuthLoginCallback() { 
	@Override 
    public void onSuccess() { } 
    @Override 
    public void onFailure(int i, @NonNull String s) { }
    @Override 
    public void onError(int i, @NonNull String s) { } });

6. 프로필 정보를 가져올 수 있다.

new NidOAuthLogin().callProfileApi(new NidProfileCallback<NidProfileResponse>() { 
	@Override 
    public void onSuccess(NidProfileResponse nidProfileResponse) { 
    	Log.d("네이버", "onSuccess: "+nidProfileResponse.getProfile().getEmail()); 
        Log.d("네이버", "onSuccess: "+nidProfileResponse.getProfile().getName()); 
        Log.d("네이버", "onSuccess: "+nidProfileResponse.getProfile().getProfileImage()); } 
    @Override 
    public void onFailure(int i, @NonNull String s) {
    
    	} 
    @Override 
    public void onError(int i, @NonNull String s) { 
    
    } 
});

 

 

 

소셜 로그인 기능 사용하기(카카오)

1. 카카오 디벨로퍼에서 애플리케이션 추가

  • 네이티브 앱 키를 사용하여 동작한다.

2. 플랫폼을 등록한다.

  • 키 해시가 필요하다
  • Signature 는 Android.Content를 임포트 한다.
private void getHashKey(){
        PackageInfo packageInfo = null;
        try {
            packageInfo = getPackageManager().getPackageInfo(getPackageName(), PackageManager.GET_SIGNATURES);
        } catch (PackageManager.NameNotFoundException e) {
            e.printStackTrace();
        }
        if (packageInfo == null)
            Log.e("KeyHash", "KeyHash:null");

        for (Signature signature : packageInfo.signatures) {
            try {
                MessageDigest md = MessageDigest.getInstance("SHA");
                md.update(signature.toByteArray());
                Log.d("KeyHash", Base64.encodeToString(md.digest(), Base64.DEFAULT));
            } catch (NoSuchAlgorithmException e) {
                Log.e("KeyHash", "Unable to get MessageDigest. signature=" + signature, e);
            }
        }
    }

3. 플랫폼을 활성화 상태로 설정해준다.

4. 카카오톡 메이븐 레퍼지토리를 setting.gradle의 repository에 추가한다.

dependencyResolutionManagement { 
	repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 
    repositories { google() mavenCentral() 
    maven { url '<https://devrepo.kakao.com/nexus/content/groups/public/>'} 
    }

 

5. 그래들로 로그인 api 디펜던시를 추가한다.

//카카오 로그인 API 
implementation "com.kakao.sdk:v2-user:2.18.0"

 

6. 카카오 로그인 객체를 생성하여준다.

  • NATIVE_APP_KEY : 카카오 디벨로퍼에 등록한 프로젝트의 네이티브앱 키를 넣어준다.
KakaoSdk.init(this, "{NATIVE_APP_KEY}")

7. 안드로이드 매니페스트 파일에 리다이렉트 url를 추가한다

<activity android:name="com.kakao.sdk.auth.AuthCodeHandlerActivity" android:exported="true"> 
	<intent-filter> 
    	<action android:name="android.intent.action.VIEW" /> 
        <category android:name="android.intent.category.DEFAULT" /> 
        <category android:name="android.intent.category.BROWSABLE" /> 
        <!-- Redirect URI: "kakao${NATIVE_APP_KEY}://oauth" --> 
        <data android:host="oauth" android:scheme="@string/NATIVE_APP_KEY" /> 
    </intent-filter> 
</activity>

8. 로그인 메소드를 작성한다.

Function2<OAuthToken, Throwable, Unit> callback = new Function2<OAuthToken, Throwable, Unit>() { 
	@Override 
    public Unit invoke(OAuthToken oAuthToken, Throwable error) { 
    	if(error==null){ 
        	Log.d("카카오", "invoke: "+oAuthToken.getAccessToken()); 
        } else { 
        	Log.d("카카오", "invoke: "+error.getMessage()); 
        } 
        return null;
    } 
}; 
//카카오톡 설치여부 확인 true->앱으로 인증(권장) 
// false->웹뷰로 인증 
if (UserApiClient.getInstance().isKakaoTalkLoginAvailable(this)) { 
	Log.d("카카오", "kakaoLogin: 카카오톡 설치됨->app"); 
    UserApiClient.getInstance().loginWithKakaoTalk(this, callback); 
} else { 
	Log.d("카카오", "kakaoLogin: 카카오톡 설치 안됨->web"); 
    UserApiClient.getInstance().loginWithKakaoAccount(this, callback); 
    UserApiClient.getInstance().me((user, throwable) -> { 
    	if(throwable==null){ 
        	Log.d("카카오정보", "invoke: "+user.getKakaoAccount().getProfile().getNickname()); 
            Log.d("카카오정보", "invoke: "+user.getKakaoAccount().getProfile().getProfileImageUrl()); 
        } else { 
        	Log.d("카카오정보", "invoke: "+throwable.getMessage()); 
        } 
        return null; 
    }); 
}

 

 

 

searchbar의 리스너 만들기

  • 쿼리텍스트리스너를 만들어준다.
  • 검색어가 바뀔 때 onQueryTextChange가 실행, 검색버튼 활성화 시 onQueryTextSubmit가 동작하게 된다.
binding.searchBar.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
            @Override
            public boolean onQueryTextSubmit(String query) {
                Log.d("검색", "onQueryTextSubmit: "+query);

                return true;
            }

            @Override
            public boolean onQueryTextChange(String newText) {
                Log.d("검색", "onQueryTextChange: "+newText);

                return true;
            }
        });

어댑터에서 동작한 후 부모 프래그먼트, 액티비티에서 반응하도록 하는 방법

  1. 고정된 부모 요소가 있는 경우 부모 요소를 생성자를 통해 가져온다.
  2. 고정되지 않은 부모 요소가 있는 경우 콜백을 이용하여 동작하도록 한다.

커스텀 다이얼로그 만들기

  • 클래스를 만들고 다이얼로그를 상속받는다.
  • 다이얼로그에 넣을 레이아웃 파일을 만들어 setContentView에 넣어준다.
  • 내부 처리를 해준다.
public class CustomerDialog extends Dialog {

    DialogCustomerUpdateBinding binding;
    public CustomerDialog(@NonNull Context context, CustomerVO vo) {
        super(context);
        binding = DialogCustomerUpdateBinding.inflate(getLayoutInflater());
        setContentView(binding.getRoot());

        binding.tvCusId.setText("고객번호:"+vo.getCustomer_id());
        binding.edtName.setText(vo.getName());
        binding.edtEmail.setText(vo.getName());
        
        binding.btnUpdate.setOnClickListener(v -> {
            vo.setName(binding.edtName.getText().toString());
            vo.setEmail(binding.edtEmail.getText().toString());
            
            new CommonConn(context, "update.cu")
                    .addParamMap("customerVO",new Gson().toJson(vo))
                    .onExcute((isResult, data) -> {
                        dismiss();//다이얼로그 숨기기
                    });
        });

    }

}

 

라디오 버튼이 있는 다이얼로그 만들기

  • AlertDialog.Builder를 이용하여 다이얼로그를 만든다.
  • setSingleChoiceItems을 이용하여 라디오버튼 선택지를 만든다.
  • 각 선택지에 따른 메소드를 작성하고 다이얼로그를 dismiss하도록 한다.
AlertDialog.Builder builder = new AlertDialog.Builder(FileActivity.this);
            builder.setTitle("사진 업로드 방식");
            builder.setSingleChoiceItems(new String[]{"갤러리", "카메라"}, -1, (dialog, i) -> {
                if(i == 0){
                    showGallary();
                } else if(i == 1){

                }
                dialog.dismiss();
            });
            AlertDialog dialog = builder.create();
            builder.show();

 

외부 기능으로 액티비티 실행하는 방법

1. startActivityForResult

  • 인텐트를 이용하여 외부 액티비티를 실행하고, startActivityForResult를 이용하여 전환한다.
  • 전환할 때 요청코드를 같이 보내고 결과를 받을 때 요청코드에 따른 메소드를 작성하게 된다.
  • RequestCode로 요청을 보내고 onActivityResult라는 메소드를 재정의한다.
  • onActivityResult에서 요청 코드에 따른 행동을 작성한다.
public void showGallary(){
        Intent intent = new Intent();
        intent.setType("image/*");
        intent.setAction(Intent.ACTION_PICK);
        startActivityForResult(intent,GALLARY_REQ);
    }
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        Log.d("갤러리", "onActivityResult: "+requestCode);
        if(requestCode == GALLARY_REQ && resultCode == RESULT_OK){
            Log.d("갤러리", "onActivityResult: "+ data.getData());
            Log.d("갤러리", "onActivityResult: "+ data.getData().getPath());

						//불러온 이미지를 이미지뷰에 붙임
            Glide.with(this).load(data.getData()).into(binding.imgv);

        } else if (requestCode == CAMERA_REQ){

        }
    }

 

2. ActivityLauncher

  • ActivityResultLauncher<Intent> 를 전역변수로 선언한다.
  • onStart에 초기화 해준다.
protected void onStart() {
        super.onStart();
        launcher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> {
            
        });
                
    }
  • 카메라실행 후 결과를 받을 Uri를 미리 지정한다.
  • 인텐트를 만들어 카메라를 실행시키도록 한다.
  • 인텐트에 putExtra로 미리 만들어 둔 카메라 uri를 넣어준다.
  • 런처를 이용하여 인텐트를 실행한다.
public void showCamera() {
        //카메라로 사용자가 사진을 찍으면 우리가 미리 임시로 만들어둔 URI에 카메라 사진을 외부 저장소에 저장 후 알려줌
        cameraUri = getContentResolver().insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, new ContentValues());
        Intent cameraIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
        cameraIntent.putExtra(MediaStore.EXTRA_OUTPUT, cameraUri);
        launcher.launch(cameraIntent);
        
    }
  • 카메라로 찍은 파일을 서버에 전송하기
  • uri가 있다면 파일 객체를 만들어서 전달한다.
File cameraFile = new File(getRealPath(cameraUri));
            //Multipart
            RequestBody file = RequestBody.create(MediaType.parse("image/jpeg"), cameraFile);
            MultipartBody.Part filePart = MultipartBody.Part.createFormData("andFile", "test.jpg", file);//name:servlet구분자, 실제 파일명, 실제 파일
            CommonService service = CommonRetroClient.getRetrofit().create(CommonService.class);
            service.clientSendFile("file.f", new HashMap<>(), filePart).enqueue(new Callback<String>() {
                @Override
                public void onResponse(Call<String> call, Response<String> response) {

                }

                @Override
                public void onFailure(Call<String> call, Throwable t) {

                }
            });

 

 

권한 부여하기

  • 중간 권한단계의 허용(YOUTUBE)
    1. 메니페스트에 명시
    2. queries에 재명시
  • 높은 권한단계의 허용(MEDIA_LOCATION, CAMERA, GPS)
    1. 메니페스트에 명시
    2. queries에 재명시
    3. 메소드를 통해 사용자의 확인을 받음
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <!-- 실제로 권한 요청을 받아와야하는것 ↓ -->
<uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION" />
<uses-permission android:name="android.permission.CAMERA" /> <!-- 카메라의 경우 안드로이드 하드웨어이기때문에 아래 속성을 추가해줘야한ㄷ. required가 false인경우 미사용도 가능. -->
<uses-feature
android:name="android.hardware.camera"
android:required="false" />
<uses-feature
android:name="android.hardware.camera2"
android:required="true" />
<queries>
	<!-- WebView -->
	<intent>
		<action android:name="android.intent.action.VIEW" />
		<category android:name="android.intent.category.BROWSABLE" />
		<data android:scheme="http" />
	</intent>
	<intent>
		<action android:name="android.intent.action.VIEW" />
		<category android:name="android.intent.category.BROWSABLE" />
		<data android:scheme="https" />
	</intent>
	<!-- Camera -->
	<intent>
		<action android:name="android.media.action.IMAGE_CAPTURE" />
	</intent>
	<!-- Gallery -->
	<intent>
		<action android:name="android.intent.action.GET_CONTENT" />
	</intent>
	<!-- Youtube -->
	<intent>
		<action android:name="android.media.browse.MediaBrowserService" />
	</intent>
</queries>
private void checkPermission(){
        ////        editor.putInt("permission" , 0);//데이터 0이 들어감.
        ////        editor.apply();//데이터를 확실히 넣음.
        int permission = pref.getInt("permission" , -1);
        permission++;
        editor.putInt("permission" , permission);
        editor.apply();

        String[] permissions = { Manifest.permission.CAMERA  ,
                                Manifest.permission.ACCESS_MEDIA_LOCATION,
                                Manifest.permission.READ_EXTERNAL_STORAGE,
                                Manifest.permission.WRITE_EXTERNAL_STORAGE

        } ;//카메라 권한을 String으로 가져옴.
        // ContextCompat(액티비티가 아닌곳) , ActivityCompat(액티비티)
        for(int i = 0 ; i <permissions.length ; i ++){
           //내가 모든 권한이 필요하다면 전체 권한을 하나씩 체크해서 허용 안됨이 있는경우 다시 요청을 하게 만든다.
            if(ActivityCompat.checkSelfPermission(this , permissions[i]) == PackageManager.PERMISSION_DENIED) {
                if(ActivityCompat.shouldShowRequestPermissionRationale(this , permissions[i])){
                    //최초 앱이 설치되고 실행 시 false가 나옴.=>사용자가 거부 후 true 재거부=>false
                    AlertDialog.Builder builder = new AlertDialog.Builder(this);
                    builder.setTitle("권한 요청").setMessage("권한이 반드시 필요합니다.!!미허용시 앱 사용 불가!");
                    builder.setPositiveButton("확인(권한허용)" , (dialog, which) -> {
                        //2.권한 설명 후 다시보여줌.
                        ActivityCompat.requestPermissions(this, permissions, REQ_PERMISSION_DENY);
                    });
                    builder.setNegativeButton("종료(권한허용불가)" , (dialog, which) -> {
                        finish();
                    });
                    builder.create().show();//<==넣어줘야함.
                }else{
                    //1.
                    ActivityCompat.requestPermissions(this, permissions, REQ_PERMISSION);
                }
                break;
            }
        }

       // int result = ActivityCompat.checkSelfPermission(this , permissions[0]);
       // Log.d("권한", "checkPermission: " + result);
//        Log.d("권한", "checkPermission: " + PackageManager.PERMISSION_GRANTED);
//        Log.d("권한", "checkPermission: " + PackageManager.PERMISSION_DENIED);
//
//        if(ActivityCompat.shouldShowRequestPermissionRationale(this , permissions[0])){
//            Log.d("권한", "shouldShowRequestPermissionRationale: 설명이 필요한 권한. ");
//            ActivityCompat.requestPermissions(this,permissions , REQ_PERMISSION);
//        }else{
//            Log.d("권한", "shouldShowRequestPermissionRationale: 설명이 x");
//            ActivityCompat.requestPermissions(this,permissions , REQ_PERMISSION);
//        }
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        if(REQ_PERMISSION == requestCode){
            for (int i = 0; i < grantResults.length; i++) {
                if(grantResults[i] == PackageManager.PERMISSION_DENIED){
                    //거절된권한이있음.
                    checkPermission();
                    break;
                }
            }

            Log.d("권한", "onRequestPermissionsResult: 권한 요청 완료 ");
        }else if(REQ_PERMISSION_DENY == requestCode){
            for (int i = 0; i < grantResults.length; i++) {
                if(grantResults[i] == PackageManager.PERMISSION_DENIED){
                    Log.d("권한", "onRequestPermissionsResult: 다시 권한요청 화면을 띄울수가 없음.2회 거절당함. ");
                    editor.putInt("permission" , -2);
                    //3.
                    viewSetting();
                    //checkPermission();
                }
            }

        }
    }

 

권한 부여 메소드의 구조

  • 권한 재확인이 필요한 권한을 배열로 가져온다.
String[] permissions = { Manifest.permission.CAMERA  ,
                Manifest.permission.ACCESS_MEDIA_LOCATION,
                Manifest.permission.READ_EXTERNAL_STORAGE,
                Manifest.permission.WRITE_EXTERNAL_STORAGE

        } ;//카메라 권한을 String으로 가져옴.
  • 설정을 열어줘서 권한을 직접 부여받도록 한다.
public void viewSetting(){
        Intent intent = new Intent(Settings.ACTION_APPLICATION_SETTINGS);
        intent.setData(Uri.parse("package:"+getApplicationContext().getPackageName()));
        startActivity(intent);
        
    }

ContentResolver를 이용하여 Uri를 실제 이미지 경로로 바꾸기

//contentReslver라는 컴포넌트를 이용하여 Uri를 통해 실제 이미지의 경로를 조회한다.
    //Android 내부에 있는 모든 요소는 전부 table 형태로 저장되어있다.
    public String getRealPath(Uri contentUri){
        String res = null;//문자열 변수로 리턴하기 위해 변수 초기화
        String[] cols = {MediaStore.Images.Media.DATA};//컬럼이름을 받아온다.(조회시 alias)
        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {//안드 API26이전은 확인 불필요. 그냥 경로를 준다.
            Cursor cursor = getContentResolver().query(contentUri, cols, null, null);
            if(cursor.moveToFirst()){
                int column_index = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA);
                res = cursor.getString(column_index);
            }
        }

        return res;
    }

 

MultiPart

  • 파일이나 이미지와 같은 바이너리 데이터를 서버에 전송할 때 사용하는 기능
  • 여러 종류의 데이터를 하나의 요청에 여러 부분으로 나눠서 전송처리를 한다.
    • 이메일을 보낼 때 텍스트 영역과 별도로 파일 첨부가 있는 형태와 유사하다
  • get요청이 불가능하다 url에 데이터 제한이 있음. post요청 필수
  • http, https 요청 : 텍스트 데이터를 주고받는데 사용한다.(파일은 이진데이터를 포함해야 한다.)

멀티파트를 이용하여 서버로 파일 전송하기

//Multipart
            RequestBody file = RequestBody.create(MediaType.parse("image/jpeg"), new File(filePath));
            MultipartBody.Part filePart = MultipartBody.Part.createFormData("andFile", "test.jpg", file);//name:servlet구분자, 실제 파일명, 실제 파일
            CommonService service = CommonRetroClient.getRetrofit().create(CommonService.class);
            service.clientSendFile("file.f", new HashMap<>(), filePart).enqueue(new Callback<String>() {
                @Override
                public void onResponse(Call<String> call, Response<String> response) {
                    
                }

                @Override
                public void onFailure(Call<String> call, Throwable t) {

                }
            });

 

서블릿에서의 멀티파트 처리

  • @MultipartConfig 어노테이션이 있어야 파일 처리가 가능하다.
  • get요청은 불가능하기 때문에 doPost를 사용하여야 한다.
  • 파일 저장은 복붙하는 것이 좋다.
@WebServlet("/file.f")
@MultipartConfig //<- MultiPart 처리는 CommonFileUpload 등 servlet 3.0 이상은 MultipartConfig만 있어도 됨
public class FileController extends HttpServlet {

	@Override
	protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
		Part part = req.getPart("andFile");
		System.out.println(part.getName()+" : "+part.getSize()+" : "+part.getContentType());
		InputStream fileContent = part.getInputStream();
		String uploadpath = "D:\\\\mid\\\\"+part.getSubmittedFileName();//저장 경로
		
		try(OutputStream out = new FileOutputStream(new File(uploadpath))){
			int read;
			final byte[] fileBytes = new byte[1024];
			while((read = fileContent.read(fileBytes)) != -1) {
				out.write(fileBytes, 0, read);
			}
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
}

 

네이버 맵

1. 네이버 클라우드 플랫폼에 가입한다.

2. 애플리케이션 등록을 하고 클라이언트 아이디, 비밀키를 받아준다.

3. setting에 레퍼지토리 추가를 해준다.

allprojects {
    repositories {
        google()
        mavenCentral()
        maven {
            url '<https://naver.jfrog.io/artifactory/maven/>'
        }
    }
}

4. 그래들 빌드에 디펜던시를 추가한다.

dependencies {
    // 네이버 지도 SDK
    implementation 'com.naver.maps:map-sdk:3.17.0'
}

5. 메니페스트의 애플리케이션 태그에 메타데이터를 추가한다.

<meta-data
            android:name="com.naver.maps.map.CLIENT_ID"
            android:value="YOUR_CLIENT_ID_HERE" />

6. 맵 api를 사용할 곳에서 지도 객체를 호출한다.

NaverMapSdk.getInstance(this).setClient(
           new NaverMapSdk.NaverCloudPlatformClient("YOUR_CLIENT_ID_HERE"));

7. 지도 뷰를 이용하여 지도를 보여줄 수 있다.

<com.naver.maps.map.MapView
    android:id="@+id/map_view"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />
binding.mapView.getMapAsync(this);

8. 맵 관련 설정을 변경하기 위해서 인터페이스를 상속받는다.

implements OnMapReadyCallback

9. 네이버 맵 사용 가이드를 보고 옵션을 변경해줄 수 있다.

  1. https://navermaps.github.io/android-map-sdk/guide-ko/2-3.html
@UiThread
    @Override
    public void onMapReady(@NonNull NaverMap naverMap) {
        naverMap.setMapType(NaverMap.MapType.Hybrid); 맵 타입 변경
        naverMap.setLayerGroupEnabled(NaverMap.LAYER_GROUP_BUILDING, false);
        naverMap.setLayerGroupEnabled(NaverMap.LAYER_GROUP_TRANSIT, true);
        naverMap.setIndoorEnabled(true);

    }

10. 초기 위치를 변경하거나 검색 시 임의의 위치로 이동하기 위해서 카메라 이동이 필요하다.

CameraUpdate cameraUpdate = CameraUpdate.scrollTo(new LatLng(34.6024675, 126.5446438))
                .animate(CameraAnimation.Easing, 1000)
                .finishCallback(() -> {
                    Toast.makeText(getContext(), "카메라 이동 완료", Toast.LENGTH_SHORT).show();
                }).cancelCallback(() -> {
                    Toast.makeText(getContext(), "카메라 이동 취소", Toast.LENGTH_SHORT).show();
                });
        new Handler().postDelayed(() -> {
            naverMap.moveCamera(cameraUpdate);

        }, 3000);

11. 사용자 인터페이스도 수정할 수 있다.

  1. https://navermaps.github.io/android-map-sdk/guide-ko/4-1.html