https://developers.google.com/maps/documentation/android-sdk/config?hl=ko

 

Android 스튜디오 프로젝트 설정  |  Android용 Maps SDK  |  Google Developers

의견 보내기 Android 스튜디오 프로젝트 설정 이 페이지에서는 빠른 시작에 자세히 설명되어 있는 Google 지도 템플릿을 사용하지 않고 Android용 Maps SDK를 사용하도록 Android 스튜디오 프로젝트를 구

developers.google.com

package com.minui.map;

import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.widget.Toast;

import com.google.android.gms.maps.CameraUpdateFactory;
import com.google.android.gms.maps.GoogleMap;
import com.google.android.gms.maps.OnMapReadyCallback;
import com.google.android.gms.maps.SupportMapFragment;
import com.google.android.gms.maps.model.BitmapDescriptorFactory;
import com.google.android.gms.maps.model.LatLng;
import com.google.android.gms.maps.model.Marker;
import com.google.android.gms.maps.model.MarkerOptions;

public class MainActivity extends AppCompatActivity implements OnMapReadyCallback, GoogleMap.OnMarkerClickListener {

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

        // 구글맵 사용하겠다는 코드작성.
        SupportMapFragment mapFragment = (SupportMapFragment) getSupportFragmentManager().findFragmentById(R.id.map);
        mapFragment.getMapAsync(this);
    }

    @Override
    public void onMapReady(@NonNull GoogleMap googleMap) {

        // 내 위치가 지도의 중심이 되도록

        LatLng myLocation = new LatLng(37.5428428, 126.6772096);

        // googleMap.moveCamera(CameraUpdateFactory.newLatLng(myLocation));
        googleMap.moveCamera(CameraUpdateFactory.newLatLngZoom(myLocation,17));

        googleMap.addMarker(new MarkerOptions().position(myLocation).title("연희직업전문학교")).setTag(0);
        googleMap.addMarker(new MarkerOptions().position(new LatLng(37.5438428, 126.6772096)).title("마커2")).setTag(1);
        googleMap.addMarker(new MarkerOptions().position(new LatLng(37.5428428, 126.6762096)).title("마커3")).setTag(2);

        googleMap.setMapType(GoogleMap.MAP_TYPE_HYBRID);

        googleMap.setOnMarkerClickListener(this);
    }

    @Override
    public boolean onMarkerClick(@NonNull Marker marker) {

        int index = (int) marker.getTag();

        if(index == 0) {
            Toast.makeText(this, "GOOD~~~~~", Toast.LENGTH_SHORT).show();
        } else {
            Toast.makeText(this, "Hello~~~~~", Toast.LENGTH_SHORT).show();
        }



        return false;
    }
}

AndroidManifest.xml에 추가

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

MainActivity.java 파일

package com.minui.location;

import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;

import android.Manifest;
import android.content.pm.PackageManager;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.os.Bundle;
import android.util.Log;

public class MainActivity extends AppCompatActivity {

    LocationManager locationManager;
    LocationListener locationListener;

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

        locationManager = (LocationManager) this.getSystemService(LOCATION_SERVICE);
        locationListener = new LocationListener() {
            @Override
            public void onLocationChanged(@NonNull Location location) {
                Log.i("myLocation", "위도 : " + location.getLatitude());
                Log.i("myLocation", "경도 : " + location.getLongitude());
            }
        };

        if (ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) {

            ActivityCompat.requestPermissions(MainActivity.this, new String[]{ Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION}, 100);

            return;
        }
        locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 3000, 3, locationListener);
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);

        if(requestCode == 100){

            if(ActivityCompat.checkSelfPermission(MainActivity.this,
                    Manifest.permission.ACCESS_FINE_LOCATION) !=
                    PackageManager.PERMISSION_GRANTED &&
                    ActivityCompat.checkSelfPermission(MainActivity.this,
                            Manifest.permission.ACCESS_COARSE_LOCATION) !=
                            PackageManager.PERMISSION_GRANTED) {

                ActivityCompat.requestPermissions(MainActivity.this,
                        new String[]{ Manifest.permission.ACCESS_FINE_LOCATION,
                                Manifest.permission.ACCESS_COARSE_LOCATION} ,
                        100);
                return;
            }

            locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER,
                    3000,
                    -1,
                    locationListener);

        }
    }
}

 

PostingApi.interface

package com.minui.photo.api;

import com.minui.photo.model.PostRes;

import okhttp3.MultipartBody;
import okhttp3.RequestBody;
import retrofit2.Call;
import retrofit2.http.Header;
import retrofit2.http.Multipart;
import retrofit2.http.POST;
import retrofit2.http.Part;

public interface PostingApi {

    @Multipart
    @POST("/posting")
    Call<PostRes> addPosting(@Header("Authorization") String token, @Part MultipartBody.Part photo, @Part("content")RequestBody content);

}

multipart를 달고 파라미터는 Part를 단다.

 

AddActivity.java의 retrofit 보내기 부분

Retrofit retrofit = NetworkClient.getRetrofitClient(AddActivity.this);
PostingApi api = retrofit.create(PostingApi.class);

// 멀티파트로 파일을 보내는 경우, 파일 파라미터 만드는 방법
RequestBody fileBody = RequestBody.create(photoFile, MediaType.parse("image/*"));
MultipartBody.Part photo = MultipartBody.Part.createFormData("photo",
        photoFile.getName(), fileBody);
// 멀티파트로 텍스트를 보내는 경우, 파라미터 만드는 방법
RequestBody contentBody = RequestBody.create(content, MediaType.parse("text/plain"));

// 헤더에 들어갈 억세스토큰 가져온다.
SharedPreferences sp = getApplication().getSharedPreferences(Config.PREFERENCE_NAME, MODE_PRIVATE);
String accessToken = sp.getString("accessToken", "");

Call<PostRes> call = api.addPosting("Bearer "+accessToken,
        photo,
        contentBody);

showProgress("포스팅 업로드 중...");

call.enqueue(new Callback<PostRes>() {
    @Override
    public void onResponse(Call<PostRes> call, Response<PostRes> response) {
        dismissProgress();

        Toast.makeText(AddActivity.this, "업로드가 완료되었습니다.", Toast.LENGTH_SHORT).show();
        finish();
    }

    @Override
    public void onFailure(Call<PostRes> call, Throwable t) {
        dismissProgress();
    }
});

주석대로 파일과 텍스트를 보내는 경우로 나눠서 파라미터를 만들고 파라미터 대입해주면 된다.

NetworkClient 파일

package com.minui.memo.api;

import android.content.Context;

import com.minui.memo.config.Config;

import java.util.concurrent.TimeUnit;

import okhttp3.OkHttpClient;
import okhttp3.logging.HttpLoggingInterceptor;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;

public class NetworkClient {

    public static Retrofit retrofit;

    public static Retrofit getRetrofitClient(Context context) {
        if(retrofit == null) {
            HttpLoggingInterceptor loggingInterceptor = new HttpLoggingInterceptor();
            loggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BASIC);

            OkHttpClient httpClient = new OkHttpClient.Builder()
                    .connectTimeout(1, TimeUnit.MINUTES)
                    .readTimeout(1, TimeUnit.MINUTES)
                    .writeTimeout(1, TimeUnit.MINUTES)
                    .addInterceptor(loggingInterceptor)
                    .build();

            retrofit = new Retrofit.Builder().baseUrl(Config.BASE_URL)
                    .client(httpClient)
                    .addConverterFactory(GsonConverterFactory.create())
                    .build();
        }
        return retrofit;
    }
}

UserApi파일

package com.minui.memo.api;

import com.minui.memo.model.RegisterRes;
import com.minui.memo.model.User;

import retrofit2.Call;
import retrofit2.http.Body;
import retrofit2.http.POST;

public interface UserApi {

    @POST("/users/register")
    Call<RegisterRes> register(@Body User user);
}

Config 파일

package com.minui.memo.config;

public class Config {

    public static final String BASE_URL = "https://5hjhcjs6xc.execute-api.us-east-1.amazonaws.com";
    public static final String TEST_URL = "http://127.0.0.1:5000";
    public static final String PREFERENCE_NAME = "memo_app";
}

RegisterRes 파일

package com.minui.memo.model;

import java.io.Serializable;

public class RegisterRes implements Serializable {

    private String result;
    private String access_token;

    public String getResult() {
        return result;
    }

    public void setResult(String result) {
        this.result = result;
    }

    public String getAccess_token() {
        return access_token;
    }

    public void setAccess_token(String access_token) {
        this.access_token = access_token;
    }
}

User 파일

package com.minui.memo.model;

import java.io.Serializable;

public class User implements Serializable {

    private String email;
    private String password;
    private String nickname;

    public User(String email, String password, String ninkname) {
        this.email = email;
        this.password = password;
        this.nickname = ninkname;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public String getNickname() {
        return nickname;
    }

    public void setNickname(String nickname) {
        this.nickname = nickname;
    }
}

MainActivity 파일 

package com.minui.memo;

import androidx.appcompat.app.AppCompatActivity;

import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;

import com.minui.memo.config.Config;

public class MainActivity extends AppCompatActivity {

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

        // 1. 쉐어드프리퍼런스에 억세스토큰을 가져온다.
        SharedPreferences sp = getApplication().getSharedPreferences(Config.PREFERENCE_NAME, MODE_PRIVATE);
        String accessToken = sp.getString("accessToken", "");

        // 2. 만약 억세스토큰이 없으면, 회원가입 액티비티를 실행하고,
        // 그렇지 않으면, 메모가져오는 API 호출해서, 리사이클러뷰로 화면에 내 메모 보여준다.
        if(accessToken.isEmpty()) {
            Intent intent = new Intent(MainActivity.this, RegisterActivity.class);
            startActivity(intent);
        } else {
            // todo 내 메모 가져오는 api 호출
        }

    }
}

RegisterActivity 파일

package com.minui.memo;

import androidx.appcompat.app.AppCompatActivity;

import android.app.ProgressDialog;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Toast;

import com.minui.memo.api.NetworkClient;
import com.minui.memo.api.UserApi;
import com.minui.memo.config.Config;
import com.minui.memo.model.RegisterRes;
import com.minui.memo.model.User;

import java.util.regex.Pattern;

import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
import retrofit2.Retrofit;

public class RegisterActivity extends AppCompatActivity {

    EditText editEmail;
    EditText editPassword;
    EditText editNickname;
    Button btnRegister;

    // 네트워크 처리 보여주는 프로그램 다이얼로그
    ProgressDialog dialog;

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

        editEmail = findViewById(R.id.editEmail);
        editPassword = findViewById(R.id.editPassword);
        editNickname = findViewById(R.id.editNickname);
        btnRegister = findViewById(R.id.btnRegister);
        btnRegister.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {

                // 이메일 가져온다. 이메일 형식체크
                String email = editEmail.getText().toString().trim();
                Pattern pattern = android.util.Patterns.EMAIL_ADDRESS;

                if(pattern.matcher(email).matches() == false) {
                    Toast.makeText(RegisterActivity.this, "이메일 형식이 올바르지 않습니다.", Toast.LENGTH_SHORT).show();
                }

                // 비밀번호 가져온다.
                String password = editPassword.getText().toString().trim();
                // 비번 길이 체크
                if(password.length() < 4 || password.length() > 12) {
                    Toast.makeText(RegisterActivity.this, "비번길이는 4자이상 12자이하로 만들어주세요.", Toast.LENGTH_SHORT).show();
                    return;
                }

                // 닉네임을 가져와서, 빈 문자열인지만 체크
                String ninkname = editNickname.getText().toString().trim();
                if (ninkname.isEmpty()) {
                    Toast.makeText(RegisterActivity.this, "닉네임 입력하세요.", Toast.LENGTH_SHORT).show();
                    return;
                }

                // 이 데이터를 API로 호출!!
                // 네트워크 통해서 데이터를 보내고 있다는,
                // 프로그레스 다이얼로그를 먼저 띄운다.
                showProgress(getString(R.string.dialog_register));

                Retrofit retrofit = NetworkClient.getRetrofitClient(RegisterActivity.this);
                UserApi api = retrofit.create(UserApi.class);

                User user = new User(email, password, ninkname);
                Call<RegisterRes> call = api.register(user);
                call.enqueue(new Callback<RegisterRes>() {
                    @Override
                    public void onResponse(Call<RegisterRes> call, Response<RegisterRes> response) {

                        dismissProgress();

                        // 200 0K 일때,
                        if(response.isSuccessful()) {
                            RegisterRes registerRes = response.body();

                            // 억세스토큰은, 이제 앱에서, api 호출할때마다 헤더에 넣어서 보내야한다.
                            // 따라서 억세스토큰은, 쉐어드프리퍼런스에 저장해 놓는다.
                            SharedPreferences sp = getApplication().getSharedPreferences(Config.PREFERENCE_NAME, MODE_PRIVATE);
                            SharedPreferences.Editor editor = sp.edit();
                            editor.putString("accessToken", registerRes.getAccess_token());
                            editor.apply();

                            finish();
                        } else if (response.code() == 400) {

                        } else {
                            Toast.makeText(RegisterActivity.this, "에러발생", Toast.LENGTH_SHORT).show();
                        }
                    }

                    @Override
                    public void onFailure(Call<RegisterRes> call, Throwable t) {
                        // 네트워크 자체 문제로 실패!
                        dismissProgress();
                    }
                });

            }
       });
    }

    void showProgress(String message) {
        dialog = new ProgressDialog(this);
        dialog.setProgressStyle(ProgressDialog.STYLE_SPINNER);
        dialog.setMessage(message);
        dialog.show();
    }

    void dismissProgress() {
        dialog.dismiss();
    }
}

gradle에

implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
implementation("com.squareup.okhttp3:logging-interceptor:4.9.0")

network_securit_config.xml파일 만들기

<?xml version="1.0" encoding="utf-8" ?>

<network-security-config>
    <domain-config cleartextTrafficPermitted="false">
        <domain includeSubdomains="true">*.amazonaws.com</domain>
    </domain-config>
</network-security-config>

안드로이드매니페스트 파일에

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    package="com.minui.memo"
    android:targetSandboxVersion="1">

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

    <application
        android:networkSecurityConfig="@xml/network_security_config"
        android:usesCleartextTraffic="true"
        android:allowBackup="true"
        android:dataExtractionRules="@xml/data_extraction_rules"
        android:fullBackupContent="@xml/backup_rules"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.Memo"
        tools:targetApi="31">
        <activity
            android:name=".RegisterActivity"
            android:exported="false" />
        <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>
    </application>

</manifest>

gadle에

implementation 'commons-io:commons-io:2.4'

fileprovider.xml에

<?xml version="1.0" encoding="utf-8"?>
<paths>
    <root-path
        name="root"
        path="." />

    <cache-path
        name="cache"
        path="." /> <!--Context.getCacheDir() 내부 저장소-->
    <files-path
        name="files"
        path="." /> <!--Context.getFilesDir() 내부 저장소-->

    <external-path
        name="external"
        path="."/>  <!--  Environment.getExternalStorageDirectory() 외부 저장소-->
    <external-cache-path
        name="external-cache"
        path="."/> <!--  Context.getExternalCacheDir() 외부 저장소-->
    <external-files-path
        name="images"
        path="Pictures" /> <!--  Context.getExternalFilesDir() 외부 저장소-->
</paths>

string.xml에 

<resources>
    <string name="app_name">MyCamera</string>
    <string name = "alert_title">선택하세요</string>

    <string-array name="alert_photo">
        <item>카메라로 사진 찍기</item>
        <item>앨범에서 가져오기</item>
    </string-array>
</resources>

AndroidManifest.xml에

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

    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
    <uses-permission android:name="android.permission.CAMERA"/>

    <application
        android:requestLegacyExternalStorage="true"
        android:allowBackup="true"
        android:dataExtractionRules="@xml/data_extraction_rules"
        android:fullBackupContent="@xml/backup_rules"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.MyCamera"
        tools:targetApi="31">

        <provider
            android:authorities="com.minui.mycamera.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/fileprovider"/>
        </provider>

        <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>
    </application>

</manifest>

MainActivity에

package com.minui.mycamera;

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

import android.Manifest;
import android.annotation.SuppressLint;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Matrix;
import android.media.ExifInterface;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.os.ParcelFileDescriptor;
import android.provider.MediaStore;
import android.provider.OpenableColumns;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.Toast;

import org.apache.commons.io.IOUtils;

import java.io.File;
import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.text.SimpleDateFormat;
import java.util.Date;

public class MainActivity extends AppCompatActivity {

    Button button;
    ImageView imageView;
    // 사진관련된 변수들
    private File photoFile;


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

        button = findViewById(R.id.button);
        imageView = findViewById(R.id.imageView);

        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                // 버튼을 누르면, 카메라에서 선택인지 앨범에서 선택인지를
                // 고를 수 있도록 알러트 다이얼로그를 띄운다.
                showDialog();

                // 카메라를 선택하면, 사진찍는 것으로
                // 앨범을 선택하면, 앨범에서 사진 선택할수 있도록 하고

                // 그 결과는 이미지뷰에 보여주도록 개발.
            }
        });

    }

    // 알러트 다이얼로그 띄우는 함수
    void showDialog(){
        AlertDialog.Builder builder = new AlertDialog.Builder(MainActivity.this);
        builder.setTitle(R.string.alert_title);
        builder.setItems(R.array.alert_photo, new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialogInterface, int i) {

                if (i == 0){
                    // todo : 사진찍는 코드 실행
                    camera();

                } else if (i == 1){
                    // todo : 앨범에서 사진 가져오는 코드 실행
                    album();
                }
            }
        });
        AlertDialog alert = builder.create();
        alert.show();
    }

    private void camera(){
        int permissionCheck = ContextCompat.checkSelfPermission(
                MainActivity.this, Manifest.permission.CAMERA);

        if(permissionCheck != PackageManager.PERMISSION_GRANTED){
            ActivityCompat.requestPermissions(MainActivity.this,
                    new String[]{Manifest.permission.CAMERA} ,
                    1000);
            Toast.makeText(MainActivity.this, "카메라 권한 필요합니다.",
                    Toast.LENGTH_SHORT).show();
            return;
        } else {
            Intent i = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
            if(i.resolveActivity(MainActivity.this.getPackageManager())  != null  ){

                // 사진의 파일명을 만들기
                String fileName = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date());
                photoFile = getPhotoFile(fileName);

                Uri fileProvider = FileProvider.getUriForFile(MainActivity.this,
                        "com.minui.mycamera.fileprovider", photoFile);
                i.putExtra(MediaStore.EXTRA_OUTPUT, fileProvider);
                startActivityForResult(i, 100);

            } else{
                Toast.makeText(MainActivity.this, "이폰에는 카메라 앱이 없습니다.",
                        Toast.LENGTH_SHORT).show();
            }
        }


    }
    private File getPhotoFile(String fileName) {
        File storageDirectory = getExternalFilesDir(Environment.DIRECTORY_PICTURES);
        try{
            return File.createTempFile(fileName, ".jpg", storageDirectory);
        }catch (IOException e){
            e.printStackTrace();
            return null;
        }
    }

    private void album(){
        if(checkPermission()){
            displayFileChoose();
        }else{
            requestPermission();
        }
    }
    private void requestPermission() {
        if(ActivityCompat.shouldShowRequestPermissionRationale(MainActivity.this,
                Manifest.permission.WRITE_EXTERNAL_STORAGE)){
            Toast.makeText(MainActivity.this, "권한 수락이 필요합니다.",
                    Toast.LENGTH_SHORT).show();
        }else{
            ActivityCompat.requestPermissions(MainActivity.this,
                    new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, 500);
        }
    }

    private boolean checkPermission(){
        int result = ContextCompat.checkSelfPermission(MainActivity.this,
                Manifest.permission.WRITE_EXTERNAL_STORAGE);
        if(result == PackageManager.PERMISSION_DENIED){
            return false;
        }else{
            return true;
        }
    }

    private void displayFileChoose() {
        Intent i = new Intent();
        i.setType("image/*");
        i.setAction(Intent.ACTION_GET_CONTENT);
        startActivityForResult(Intent.createChooser(i, "SELECT IMAGE"), 300);
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        switch (requestCode) {
            case 1000: {
                if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                    Toast.makeText(MainActivity.this, "권한 허가 되었음",
                            Toast.LENGTH_SHORT).show();
                } else {
                    Toast.makeText(MainActivity.this, "아직 승인하지 않았음",
                            Toast.LENGTH_SHORT).show();
                }
                break;
            }
            case 500: {
                if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                    Toast.makeText(MainActivity.this, "권한 허가 되었음",
                            Toast.LENGTH_SHORT).show();
                } else {
                    Toast.makeText(MainActivity.this, "아직 승인하지 않았음",
                            Toast.LENGTH_SHORT).show();
                }

            }

        }
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
        if(requestCode == 100 && resultCode == RESULT_OK){

            Bitmap photo = BitmapFactory.decodeFile(photoFile.getAbsolutePath());

            ExifInterface exif = null;
            try {
                exif = new ExifInterface(photoFile.getAbsolutePath());
            } catch (IOException e) {
                e.printStackTrace();
            }
            int orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION,
                    ExifInterface.ORIENTATION_UNDEFINED);
            photo = rotateBitmap(photo, orientation);

            // 압축시킨다. 해상도 낮춰서
            OutputStream os;
            try {
                os = new FileOutputStream(photoFile);
                photo.compress(Bitmap.CompressFormat.JPEG, 50, os);
                os.flush();
                os.close();
            } catch (Exception e) {
                Log.e(getClass().getSimpleName(), "Error writing bitmap", e);
            }

            photo = BitmapFactory.decodeFile(photoFile.getAbsolutePath());

            imageView.setImageBitmap(photo);
            imageView.setScaleType(ImageView.ScaleType.FIT_XY);

            // 네트워크로 데이터 보낸다.



        }else if(requestCode == 300 && resultCode == RESULT_OK && data != null &&
                data.getData() != null){

            Uri albumUri = data.getData( );
            String fileName = getFileName( albumUri );
            try {

                ParcelFileDescriptor parcelFileDescriptor = getContentResolver( ).openFileDescriptor( albumUri, "r" );
                if ( parcelFileDescriptor == null ) return;
                FileInputStream inputStream = new FileInputStream( parcelFileDescriptor.getFileDescriptor( ) );
                photoFile = new File( this.getCacheDir( ), fileName );
                FileOutputStream outputStream = new FileOutputStream( photoFile );
                IOUtils.copy( inputStream, outputStream );

//                //임시파일 생성
//                File file = createImgCacheFile( );
//                String cacheFilePath = file.getAbsolutePath( );


                // 압축시킨다. 해상도 낮춰서
                Bitmap photo = BitmapFactory.decodeFile(photoFile.getAbsolutePath());
                OutputStream os;
                try {
                    os = new FileOutputStream(photoFile);
                    photo.compress(Bitmap.CompressFormat.JPEG, 60, os);
                    os.flush();
                    os.close();
                } catch (Exception e) {
                    Log.e(getClass().getSimpleName(), "Error writing bitmap", e);
                }

                imageView.setImageBitmap(photo);
                imageView.setScaleType(ImageView.ScaleType.FIT_XY);

//                imageView.setImageBitmap( getBitmapAlbum( imageView, albumUri ) );

            } catch ( Exception e ) {
                e.printStackTrace( );
            }

            // 네트워크로 보낸다.
        }
        super.onActivityResult(requestCode, resultCode, data);
    }


    public static Bitmap rotateBitmap(Bitmap bitmap, int orientation) {

        Matrix matrix = new Matrix();
        switch (orientation) {
            case ExifInterface.ORIENTATION_NORMAL:
                return bitmap;
            case ExifInterface.ORIENTATION_FLIP_HORIZONTAL:
                matrix.setScale(-1, 1);
                break;
            case ExifInterface.ORIENTATION_ROTATE_180:
                matrix.setRotate(180);
                break;
            case ExifInterface.ORIENTATION_FLIP_VERTICAL:
                matrix.setRotate(180);
                matrix.postScale(-1, 1);
                break;
            case ExifInterface.ORIENTATION_TRANSPOSE:
                matrix.setRotate(90);
                matrix.postScale(-1, 1);
                break;
            case ExifInterface.ORIENTATION_ROTATE_90:
                matrix.setRotate(90);
                break;
            case ExifInterface.ORIENTATION_TRANSVERSE:
                matrix.setRotate(-90);
                matrix.postScale(-1, 1);
                break;
            case ExifInterface.ORIENTATION_ROTATE_270:
                matrix.setRotate(-90);
                break;
            default:
                return bitmap;
        }
        try {
            Bitmap bmRotated = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true);
            bitmap.recycle();
            return bmRotated;
        }
        catch (OutOfMemoryError e) {
            e.printStackTrace();
            return null;
        }
    }

    //앨범에서 선택한 사진이름 가져오기
    public String getFileName( Uri uri ) {
        Cursor cursor = getContentResolver( ).query( uri, null, null, null, null );
        try {
            if ( cursor == null ) return null;
            cursor.moveToFirst( );
            @SuppressLint("Range") String fileName = cursor.getString( cursor.getColumnIndex( OpenableColumns.DISPLAY_NAME ) );
            cursor.close( );
            return fileName;

        } catch ( Exception e ) {
            e.printStackTrace( );
            cursor.close( );
            return null;
        }
    }

    //이미지뷰에 뿌려질 앨범 비트맵 반환
    public Bitmap getBitmapAlbum( View targetView, Uri uri ) {
        try {
            ParcelFileDescriptor parcelFileDescriptor = getContentResolver( ).openFileDescriptor( uri, "r" );
            if ( parcelFileDescriptor == null ) return null;
            FileDescriptor fileDescriptor = parcelFileDescriptor.getFileDescriptor( );
            if ( fileDescriptor == null ) return null;

            int targetW = targetView.getWidth( );
            int targetH = targetView.getHeight( );

            BitmapFactory.Options options = new BitmapFactory.Options( );
            options.inJustDecodeBounds = true;

            BitmapFactory.decodeFileDescriptor( fileDescriptor, null, options );

            int photoW = options.outWidth;
            int photoH = options.outHeight;

            int scaleFactor = Math.min( photoW / targetW, photoH / targetH );
            if ( scaleFactor >= 8 ) {
                options.inSampleSize = 8;
            } else if ( scaleFactor >= 4 ) {
                options.inSampleSize = 4;
            } else {
                options.inSampleSize = 2;
            }
            options.inJustDecodeBounds = false;

            Bitmap reSizeBit = BitmapFactory.decodeFileDescriptor( fileDescriptor, null, options );

            ExifInterface exifInterface = null;
            try {
                if ( Build.VERSION.SDK_INT >= Build.VERSION_CODES.N ) {
                    exifInterface = new ExifInterface( fileDescriptor );
                }
            } catch ( IOException e ) {
                e.printStackTrace( );
            }

            int exifOrientation;
            int exifDegree = 0;

            //사진 회전값 구하기
            if ( exifInterface != null ) {
                exifOrientation = exifInterface.getAttributeInt( ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL );

                if ( exifOrientation == ExifInterface.ORIENTATION_ROTATE_90 ) {
                    exifDegree = 90;
                } else if ( exifOrientation == ExifInterface.ORIENTATION_ROTATE_180 ) {
                    exifDegree = 180;
                } else if ( exifOrientation == ExifInterface.ORIENTATION_ROTATE_270 ) {
                    exifDegree = 270;
                }
            }

            parcelFileDescriptor.close( );
            Matrix matrix = new Matrix( );
            matrix.postRotate( exifDegree );

            Bitmap reSizeExifBitmap = Bitmap.createBitmap( reSizeBit, 0, 0, reSizeBit.getWidth( ), reSizeBit.getHeight( ), matrix, true );
            return reSizeExifBitmap;

        } catch ( Exception e ) {
            e.printStackTrace( );
            return null;
        }


    }

}

 

package com.minui.youtube;

import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.widget.NestedScrollView;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;

import android.os.Build;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.ProgressBar;
import android.widget.ScrollView;
import android.widget.Toast;

import com.android.volley.Request;
import com.android.volley.RequestQueue;
import com.android.volley.Response;
import com.android.volley.toolbox.JsonObjectRequest;
import com.android.volley.toolbox.Volley;
import com.minui.youtube.adapter.YoutubeAdapter;
import com.minui.youtube.model.Youtube;

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

import java.util.ArrayList;

public class MainActivity extends AppCompatActivity {

    RecyclerView recyclerView;
    YoutubeAdapter adapter;
    ArrayList<Youtube> youtubeList;
    ProgressBar progressBar;
    EditText editSearch;
    ImageView imgSearch;
    String pageToken = null;
    String keyword;

    final String URL = "https://www.googleapis.com/youtube/v3/search?part=snippet&key=AIzaSyDmItl5QNonjtpgje3I2jyCP6gDV-7wbOo&q=";

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

        recyclerView = findViewById(R.id.recyclerView);
        recyclerView.setHasFixedSize(true);
        recyclerView.setLayoutManager(new LinearLayoutManager(MainActivity.this));
        // 리스트를 맨 밑에까지 가면, 알 수 있는 방법!
        recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
            @Override
            public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
                super.onScrollStateChanged(recyclerView, newState);
            }

            @Override
            public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
                super.onScrolled(recyclerView, dx, dy);

                int lastPosition = ((LinearLayoutManager)recyclerView.getLayoutManager()).findLastCompletelyVisibleItemPosition();
                int totalCount = recyclerView.getAdapter().getItemCount();

                // 스크롤을 맨 끝까지 한 것!
                if(lastPosition + 1 == totalCount) {
                    if (pageToken == null) {
                        return;
                    }
                    String apiUrl;

                    keyword = editSearch.getText().toString().trim();


                    apiUrl = URL + keyword + "&maxResults=20&pageToken=" + pageToken;

                    RequestQueue requestQueue = Volley.newRequestQueue(MainActivity.this);
                    JsonObjectRequest jsonObjectRequest = new JsonObjectRequest(Request.Method.GET, apiUrl , null, new Response.Listener<JSONObject>() {
                        @Override
                        public void onResponse(JSONObject response) {
                            try {
                                JSONArray jsonArr = response.getJSONArray("items");
                                if (response.has("nextPageToken")) {
                                    pageToken = response.getString("nextPageToken");
                                }

                                for(int i=0;i<jsonArr.length();i++){

                                    JSONObject jsonObj = jsonArr.getJSONObject(i);
                                    JSONObject snippet = jsonObj.getJSONObject("snippet");
                                    JSONObject thumbnails = snippet.getJSONObject("thumbnails");
                                    JSONObject id = jsonObj.getJSONObject("id");
                                    JSONObject defaultThumb = thumbnails.getJSONObject("default");
                                    JSONObject bigThumb = thumbnails.getJSONObject("medium");

                                    Youtube youtube = new Youtube(snippet.getString("title"), snippet.getString("description"), defaultThumb.getString("url"), bigThumb.getString("url"), id.getString("videoId"));
                                    youtubeList.add(youtube);

                                }

                            } catch (JSONException e) {
                                Toast.makeText(getApplicationContext(), "네트워크 에러입니다.", Toast.LENGTH_SHORT).show();
                                return;
                            } finally {
                                progressBar.setVisibility(View.INVISIBLE);
                            }

                            adapter.notifyDataSetChanged();

                        }
                    }, null);
                    progressBar.setVisibility(View.VISIBLE);
                    requestQueue.add(jsonObjectRequest);
                }
            }
        });

        youtubeList = new ArrayList<>();
        progressBar = findViewById(R.id.progressBar);
        editSearch = findViewById(R.id.editSearch);
        imgSearch = findViewById(R.id.imgSearch);

        imgSearch.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {

                String apiUrl;

                keyword = editSearch.getText().toString().trim();
                apiUrl = URL + keyword + "&maxResults=20";

                youtubeList.clear();
                if (adapter != null){
                    adapter.notifyDataSetChanged();
                }

                RequestQueue requestQueue = Volley.newRequestQueue(MainActivity.this);
                JsonObjectRequest jsonObjectRequest = new JsonObjectRequest(Request.Method.GET, apiUrl , null, new Response.Listener<JSONObject>() {
                    @Override
                    public void onResponse(JSONObject response) {
                        try {
                            JSONArray jsonArr = response.getJSONArray("items");
                            if (response.has("nextPageToken")) {
                                pageToken = response.getString("nextPageToken");
                            }

                            for(int i=0;i<jsonArr.length();i++){

                                JSONObject jsonObj = jsonArr.getJSONObject(i);
                                JSONObject snippet = jsonObj.getJSONObject("snippet");
                                JSONObject thumbnails = snippet.getJSONObject("thumbnails");
                                JSONObject id = jsonObj.getJSONObject("id");
                                JSONObject defaultThumb = thumbnails.getJSONObject("default");
                                JSONObject bigThumb = thumbnails.getJSONObject("medium");

                                Youtube youtube = new Youtube(snippet.getString("title"), snippet.getString("description"), defaultThumb.getString("url"), bigThumb.getString("url"), id.getString("videoId"));
                                youtubeList.add(youtube);

                            }

                        } catch (JSONException e) {
                            Toast.makeText(getApplicationContext(), "네트워크 에러입니다.", Toast.LENGTH_SHORT).show();
                            return;
                        } finally {
                            progressBar.setVisibility(View.INVISIBLE);
                        }

                        adapter = new YoutubeAdapter(MainActivity.this, youtubeList);
                        recyclerView.setAdapter(adapter);

                    }
                }, null);
                progressBar.setVisibility(View.VISIBLE);
                requestQueue.add(jsonObjectRequest);
            }
        });

    }
}

recyclerView.addOnScrollListener를 작성하고

onScrolled 안의 조건문에서 스크롤 맨 끝 일때 데이터를 불러오면 된다.

dependencies {
  implementation 'com.github.bumptech.glide:glide:4.13.2'
  annotationProcessor 'com.github.bumptech.glide:compiler:4.13.2'
}

먼저 라이브러리 추가 해준다.

GlideUrl url = new GlideUrl(photo.thumbnailUrl, new LazyHeaders.Builder().addHeader("User-Agent", "Andorid").build());
Glide.with(context).load(url).into(holder.imgThumb);

GlideUrl 객체만들고 url 불러오고 이미지에 대입한다.

FloatingActionButton 끌어오고 id 지정해준다.

fab.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Intent intent = new Intent(MainActivity.this, AddActivity.class);
                activityResultLauncher.launch(intent);
            }
        });

fab으로 변수만들고 객체 대입해준다음 리스너 만든다

res -> new -> Android Resource 눌르고

fil name 적고 Resource type을 Menu로 고르고 ok 한다.

원하는 ui배치하고 id와 title을 정해준다.icon과 showAsAction도 정해준다.

// xml로 만든 메뉴를, 액티비티의 화면에 나타나게 해주는 함수!
    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.main, menu);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(@NonNull MenuItem item) {
        int itemId = item.getItemId();

        if (itemId == R.id.menuAdd) {
            Intent intent = new Intent(MainActivity.this, AddActivity.class);
            activityResultLauncher.launch(intent);
        } else if (itemId == R.id.menuAbout) {
            // todo : About 클릭했을때 하고 싶은 일 코드 작성.
        }

        return super.onOptionsItemSelected(item);
    }

메뉴를 나타나게 해주고 아이템 선택되었을 때 코드를 작성해 준다.

+ Recent posts