728x90

날짜를 선택하여 입력하는 EditText 예제이다.


<LinearLayout
    android:layout_width="match_parent"
    android:layout_height="40dp"
    android:layout_marginTop="10dp"
    android:layout_marginBottom="5dp"
    android:orientation="horizontal">

    <TextView
        android:layout_width="70dp"
        android:layout_height="30dp"
        android:layout_marginTop="5dp"
        android:text="운행일자"
        android:textSize="18sp"
        android:textStyle="bold"
        android:textColor="@color/colorBlack"/>

    <EditText
        android:id="@+id/et_drivingDate"
        android:layout_width="120dp"
        android:layout_height="30dp"
        android:layout_marginTop="5dp"
        android:layout_marginLeft="20dp"
        android:background="#E3E8EA"
        android:gravity="center"
        android:focusable="false"
        android:inputType="date"/>

</LinearLayout> 


public class MainActivity extends AppCompatActivity {
    private final String TAG = this.getClass().getSimpleName();
    Context mContext;

    //Declaring EditText
    private EditText et_drivingDate;
    String drivingDate; // 운행일자
    final Calendar myCalendar = Calendar.getInstance();


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mContext = MainActivity.this;
        initView();
    }

    private void updateLabel() {
        String myFormat = "yyyy-MM-dd"; //In which you need put here
        SimpleDateFormat sdf = new SimpleDateFormat(myFormat, Locale.US);

        et_drivingDate.setText(sdf.format(myCalendar.getTime()));
    }

    private void initView() {
        //Initializing the views
        et_drivingDate = findViewById(R.id.et_drivingDate);
        DatePickerDialog.OnDateSetListener date = new DatePickerDialog.OnDateSetListener() {

            @Override
            public void onDateSet(DatePicker view, int year, int monthOfYear, int dayOfMonth) {
                myCalendar.set(Calendar.YEAR, year);
                myCalendar.set(Calendar.MONTH, monthOfYear);
                myCalendar.set(Calendar.DAY_OF_MONTH, dayOfMonth);
                updateLabel();
            }

        };
        et_drivingDate.setOnClickListener(new View.OnClickListener() {

            @Override
            public void onClick(View v) {
                new DatePickerDialog(MainActivity.this, date, myCalendar
                        .get(Calendar.YEAR), myCalendar.get(Calendar.MONTH),
                        myCalendar.get(Calendar.DAY_OF_MONTH)).show();
            }
        });

    }
}


'안드로이드 > Layout' 카테고리의 다른 글

LinearLayout 동적 생성  (0) 2020.10.01
SMS authentication with OTP Layout Example  (0) 2020.09.14
Floating Action Button  (0) 2020.07.11
CardView Layout 예제  (0) 2020.04.12
Meterial Design 로그인 Layout 예제  (0) 2020.03.21
블로그 이미지

Link2Me

,
728x90

LinearLayoutManager mLayoutManager = new LinearLayoutManager(this);
mLayoutManager.setReverseLayout(true);
mLayoutManager.setStackFromEnd(true);
// Set the layout manager to your recyclerview
recyclerView.setLayoutManager(mLayoutManager);


출처 : https://freakycoder.com/android-notes-46-how-to-reverse-recyclerview-by-adding-items-f32db1e36c51

블로그 이미지

Link2Me

,
728x90

private val groupList: Unit
    private get() {
        remoteService = RetrofitAPI.getClient().create(RemoteService::class.java)
        val groupResultCall = remoteService?.getGroupList(Value.encrypt(Value.URLkey()))
        groupResultCall?.enqueue(object : Callback<GroupResult> {
            override fun onResponse(call: Call<GroupResult>, response: Response<GroupResult>) {
                if (response.body()!!.status.contains("success")) {
                    groupItem.clear()
                    val groupData = response.body()!!.message
                    for (item in groupData) {
                        groupItem.add(item)
                    }
                    Log.d("TAG", "Number of groupItem :" + groupItem.size)
                }
            }

            override fun onFailure(call: Call<GroupResult>,t: Throwable) {
            }
        })
    }
 


Java로 작성한 코드를 코틀린으로 변환하여 테스트하고 적어둔다.


override fun onClick(view: View) {
    when (view.id) {
        R.id.btn_drawer -> drawer_layout.openDrawer(GravityCompat.START)
        R.id.btn_group -> {
            // 팝업 메뉴 나오게 하는 방법
            Log.d("TAG", "popupMenu of groupItem :" + groupItem.size)
            val popupMenu = PopupMenu(this, view)
            val menu = popupMenu.menu
            var i = 0
            while (i < groupItem.size) {
                menu.add(Menu.NONE,i,Menu.NONE,groupItem[i].name)
                i++
            }
            popupMenu.setOnMenuItemClickListener { item: MenuItem ->
                val i = item.itemId
                freeActiveMarkers()
                getAddrData(item.title.toString())
                LocationAt(groupItem[i].latitude.toDouble(),groupItem[i].longitude.toDouble())
                Toast.makeText(context,item.title.toString() + " 선택했습니다",Toast.LENGTH_SHORT).show()
                true
            }
            popupMenu.show()
        }
    }
}


private fun LocationAt(latitude: Double, longitude: Double) {
    val coord = LatLng(latitude, longitude)
    mNaverMap!!.moveCamera(CameraUpdate.scrollTo(coord).animate(CameraAnimation.Easing, 300))
}


PHP 서버코드

<?php
error_reporting(0);
//error_reporting(E_ALL);
//ini_set("display_errors", 1);
header("Cache-Control: no-cache, must-revalidate");
header("Content-type: application/json; charset=utf-8");

// 파일을 직접 실행하는 비정상적 동작을 방지 하기 위한 목적
if(isset($_POST) && $_SERVER['REQUEST_METHOD'] == "POST"){
    @extract($_POST);
    require_once 'path.php';// root 폴더를 기준으로 상대적인 경로 자동 구하기
    require_once $g['path_config'].'config.php';
    require_once $g['path_class'].'dbconnect.php';
    require_once $g['path_class'].'loginClass.php';
    $c = new LoginClass;

    //키워드 확인
    if(!isset($_POST['keyword'])){
        $result = array(
            'status' => "fail",
            'message' => "no keyword"
        );
        echo json_encode($result);
        exit;
    }

    $keyword=$c->AES_Decode($_POST['keyword']);
    //키워드 일치 확인
    if(strcmp($keyword,$mykey)<>0){
        $result = array(
            'status' => "keyfail",
            'message' => "서버와 단말의 KEY가 일치하지 않습니다."
        );
        echo json_encode($result);
        exit;
    }

    // 배열 저장
    $R = array();
    array_push($R,array("name"=>"전체","latitude"=>"37.358752","longitude"=>"127.1149243"));
    $map_sql = "select name,latitude,longitude from mapTree";
    $map_result = mysqli_query($db,$map_sql);
    while($row = mysqli_fetch_row($map_result)) {
        array_push($R,array("name"=>$row[0],"latitude"=>$row[1],"longitude"=>$row[2]));
    }

    $status = "success";
    $message = $R;
   
    $result = array(
        'status' => $status,
        'message' => $message
    );
    echo json_encode($result);
}
?> 


블로그 이미지

Link2Me

,
728x90

y = f(x) 형태의 함수로 구성된 프로그래밍 기법

Java 8에서 함수형 프로그래밍 방식인 람다식을 지원한다.

객체지향 프로그래밍보다는 함수형 프로그래밍에 가깝다고 할 수 있다.

 

람다식이란? 함수(메서드)를 간단한 ‘식(Expression)’으로 표현하는 방법

 

- 함수는 클래스에 독립적, 메소드는 클래스에 종속적
- 메서드의 이름과 반환타입을 제거하고 ‘->’를 블록{} 앞에 추가한다.
- 반환값이 있는 경우, 식이나 값만 적고 return문 생략 가능(끝에‘;’안 붙임)
- 매개변수의 타입이 추론 가능하면 생략가능(대부분의 경우 생략가능)
- 매개변수가 하나인 경우, 괄호() 생략가능(타입이 없을 때만)
- 블록 안의 문장이 하나뿐 일 때, 괄호{}생략가능(끝에 ‘;’ 안 붙임)
- 단, 하나뿐인 문장이 return문이면 괄호{} 생략불가

 

 

람다식 장점
1. 코드를 간결하게 만들 수 있다.
2. 코드가 간결하고 식에 개발자의 의도가 명확히 드러나므로 가독성이 향상된다.
3. 함수를 만드는 과정없이 한번에 처리할 수 있기에 코딩하는 시간이 줄어든다.
4. 병렬프로그래밍이 용이하다.

람다식 단점
1. 람다를 사용하면서 만드는 익명함수는 재사용이 불가능하다.
2. 디버깅이 다소 까다롭다.
3. 람다를 남발하면 코드가 지저분해질 수 있다.
4. 재귀로 만들경우에는 다소 부적합한면이 있다.

 

 
@FunctionalInterface
interface MyFunction {
    // 함수형 인터페이스 : 단 하나의 추상 메소드만 선언된 인터페이스
    int max(int a, int b);  // public abstract 생략
}
 
public class LambdaEX1 {
    public static void main(String[] args) {
        // 람다식(익명 객체)을 다루기 위한 참조변수의 타입은 함수형 인터페이스로 한다.
        MyFunction f = (a, b) -> a > b ? a : b;
        int value = f.max(3,5);
        System.out.println(value);
    }
}
 

 

@FunctionalInterface
interface MyFunctionalInterface{
    // 함수형 인터페이스 : 단 하나의 추상 메소드만 선언된 인터페이스
    void method();
}
 
public class LambdaEX2 {
    public static void main(String[] args) {
        MyFunctionalInterface fi1 = new MyFunctionalInterface() {
            @Override
            public void method() {
                String str = "직접 익명 객체 구현함";
                System.out.println(str);
            }
        };
        fi1.method();
 
        MyFunctionalInterface fi2 = () -> {
            String str = "람다식 익명 객체 구현함";
            System.out.println(str);
        };
        fi2.method();
    }
}
 

 

 

 

 

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
 
@FunctionalInterface
interface MyFunctionIF {
    // 함수형 인터페이스 : 단 하나의 추상 메소드만 선언된 인터페이스
    int max(int a, int b);  // public abstract 생략
}
 
public class LambdaEX1 {
    public static void main(String[] args) {
        // 람다식(익명 객체)을 다루기 위한 참조변수의 타입은 함수형 인터페이스로 한다.
        MyFunctionIF f = (a, b) -> a > b ? a : b;
        int value = f.max(3,5);
        System.out.println(value);
 
        int result = Integer.parseInt("123");
        System.out.println(result);
 
        System.out.println("===================================");
        ArrayList<Integer> list = new ArrayList<>();
        for(int i=0;i < 20;i++)
            list.add(i);
 
        System.out.println(list);
 
        // list의 모든 요소를 출력
        list.forEach(i->System.out.print(i + ", "));
        System.out.println();
 
        // list에서 2 또는 3의 배수를 제거한다.
        list.removeIf(x -> x%2==0 || x%3==0);
        System.out.println(list);
 
        list.replaceAll(i->i*3); // list의 각 요소에 3을 곱한다.
        System.out.println(list);
 
        System.out.println("===================================");
        Map<StringString> map = new HashMap<>();
        map.put("1""1");
        map.put("2""2");
        map.put("3""3");
        map.put("4""4");
 
        // map의 모든 요소를 {k,v}의 형식으로 출력한다.
        map.forEach((k,v)-> System.out.print("{"+k+","+v+"},"));
        System.out.println();
    }
}
 

 

메소드 참조

메소드 레퍼런스(Method Reference)는 Lambda 표현식을 더 간단하게 표현하는 방법이다.

메소드 레퍼런스는 ClassName::MethodName 형식으로 입력한다. 
메소드를 호출하는 것이지만 괄호()는 써주지 않고 생략한다.

import java.util.Arrays;
import java.util.List;
import java.util.function.BinaryOperator;
import java.util.function.IntBinaryOperator;
 
class MathBox {
    // 클래스::메소드 --> 정적(static) 메소드 참조
    public static int staticPlus(int x, int y){
        return x+y;
    }
 
    // 참조변수::메소드 --> 인스턴스 메소드 참조
    public int sum(int x, int y){
        return x+y;
    }
 
    // 정수 빼기
    public int sub(int x, int y){
        return x-y;
    }
 
    // 정수 나누기
    public int division(int x, int y){
        return x / y;
    }
 
    // 실수 나누기
    public Float float_div(Float x, Float y){
        return x / y;
    }
 
}
 
public class Method_References {
    public static void main(String[] args) {
        List<String> strList = Arrays.asList("안녕""하세요");
        strList.stream().forEach(System.out::println);
 
        // static 메소드 참조
        IntBinaryOperator operator1;
        // 메소드 참조해서 매개변수 리턴타입 알아내 람다식에서 불필요한 매개변수 제거 목적
        operator1 = MathBox::staticPlus; // 메소드 참조
        System.out.println(operator1.applyAsInt(53));
 
        operator1 = (x, y) -> MathBox.staticPlus(x, y);
        System.out.println(operator1.applyAsInt(35));
 
        // instance 메소드 참조
        MathBox calculator = new MathBox();
        IntBinaryOperator operator2 = calculator::sum; // 메소드 참조
        System.out.println("더하기 : " + operator2.applyAsInt(1132));
 
        operator1 = (x, y) -> calculator.sum(x, y);
        System.out.println(operator1.applyAsInt(1132));
 
        operator2 = calculator::sub; // 메소드 참조
        System.out.println("빼기 : " + operator2.applyAsInt(1130));
 
        operator2 = calculator::division; // 메소드 참조
        System.out.println("나누기 : " + operator2.applyAsInt(154));
 
        BinaryOperator<Integer> operator3 = (n1, n2) -> n1 + n2; // 타입 추론
        Integer sum = operator3.apply(10100);
        System.out.println(sum);
 
        BinaryOperator<Float> operator4 = calculator::float_div;
        System.out.println("나누기 : " + operator4.apply(15.0f, 4.0f));
 
        BinaryOperator<Float> operator5 = (n1, n2) -> n1 / n2; // 타입 추론
        Float div = operator5.apply(15.0f, 4.0f);
        System.out.println(div);
    }
}
 

 

 

자바에서 제공하는 함수형 인터페이스를 이용하면 인터페이스를 만들지 않고 간단하게 코드 구현 가능하다.

import java.util.function.Consumer;
 
interface Printable {
    void doSomething(String text);
}
 
public class MethodReferencesEX2 {
    public static class Printer {
        static void printSomething(String text) {
            System.out.println(text);
        }
    }
 
    public static void main(String[] args) {
        Printable printable = text -> Printer.printSomething(text);
        printable.doSomething("do something");
 
        Printable printable2 = Printer::printSomething;
        printable2.doSomething("do something");
 
        // Consumer<클래스>는 자바에서 제공하는 함수형 인터페이스
        // String 1개를 인자로 받아 void를 리턴하는 메소드를 갖고 있는 인터페이스
        Consumer<String> consumer = Printer::printSomething;
        consumer.accept("do something"); // Printable 인터페이스를 만들 필요가 없다.
    }
}
 

 

블로그 이미지

Link2Me

,
728x90

Retrofit2 라이브러리 Java 버전 코딩을 Kotlin 으로 변환해서 이용하는 법을 적어둔다.

Android Studio 에서 Java 파일 Kotlin 파일로 변환하는 기능을 이용하여 변환하고 나서 에러가 나는 부분을 수정하면 대부분 해결이 된다.

static 은 object 파일로 변환을 해버리는데, 여기서는 companion object 를 사용해서 수동 변환을 했다.


public class RetrofitAPI {
    private static Retrofit retrofit = null;

    public static Retrofit getClient() {
        retrofit = new Retrofit.Builder()
                .baseUrl(Value.IPADDRESS)
                .addConverterFactory(GsonConverterFactory.create())
                .client(createOkHttpClient())
                .build();

        return retrofit;
    }

    private static OkHttpClient createOkHttpClient() {
        // 네트워크 통신 로그(서버로 보내는 파라미터 및 받는 파라미터) 보기
        OkHttpClient.Builder builder = new OkHttpClient.Builder();
        HttpLoggingInterceptor interceptor = new HttpLoggingInterceptor();
        interceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
        builder.addInterceptor(interceptor);
        return builder.build();
    }
}

class RetrofitAPI {

    companion object{
        private var retrofit: Retrofit? = null

        fun getClient(): Retrofit? {
            retrofit = Retrofit.Builder()
                .baseUrl(Value.IPADDRESS)
                .addConverterFactory(GsonConverterFactory.create())
                .client(createOkHttpClient())
                .build()
            return retrofit
        }

        private fun createOkHttpClient(): OkHttpClient? {
            // 네트워크 통신 로그(서버로 보내는 파라미터 및 받는 파라미터) 보기
            val builder = OkHttpClient.Builder()
            val interceptor =
                HttpLoggingInterceptor()
            interceptor.setLevel(HttpLoggingInterceptor.Level.BODY)
            builder.addInterceptor(interceptor)
            return builder.build()
        }
    }

}


public interface RemoteService {// 서버에 호출할 메소드를 선언하는 인터페이스
    // POST 방식으로 데이터를 주고 받을 때 넘기는 변수는 Field 라고 해야 한다.
    @FormUrlEncoded
    @POST(RetrofitUrl.URL_LOGIN)
    Call<LoginResult> Login(
            @Field("userid") String userId,
            @Field("userpw") String userPw,
            @Field("token") String token,
            @Field("keyword") String keyword,
            @Field("mfoneNO") String mfoneNO,
            @Field("uuid") String uuid
    );
}

interface RemoteService { // 서버에 호출할 메소드를 선언하는 인터페이스
    // POST 방식으로 데이터를 주고 받을 때 넘기는 변수는 Field 라고 해야 한다.
    @FormUrlEncoded
    @POST(RetrofitUrl.URL_LOGIN)
    fun Login(
        @Field("userid") userId: String,
        @Field("userpw") userPw: String,
        @Field("token") token: String,
        @Field("keyword") keyword: String,
        @Field("mfoneNO") mfoneNO: String,
        @Field("uuid") uuid: String
    ): Call<LoginResult>
}



로그인 예제 변환

void AutoLoginProgress() { // 자동 로그인 체크 검사
    //Log.e("AutoLogin", "Auto Login Stage.");
    pref = getSharedPreferences("pref", Activity.MODE_PRIVATE);
    userID = pref.getString("userid", "");
    userPW = pref.getString("userpw", "");
    String keyword = Value.encrypt(Value.URLkey());
    String mfoneNO = Value.encrypt(getPhoneNumber());
    uID = Settings.Secure.getString(getApplicationContext().getContentResolver(), Settings.Secure.ANDROID_ID);

    if (userID != null && !userID.isEmpty() && userPW != null && !userPW.isEmpty()) {
        mloginService = RetrofitAPI.getClient().create(RemoteService.class);
        mloginService.Login(Value.encrypt(userID), Value.encrypt(userPW),"", keyword, mfoneNO, Value.encrypt(uID))
                .enqueue(new Callback<LoginResult>() {
                    @Override
                    public void onResponse(Call<LoginResult> call, Response<LoginResult> response) {
                        // 네트워크 통신 성공
                        LoginResult result = response.body();
                        if(result.getResult().contains("success")){

                            Log.e("Intor UserName",result.getUserinfo().getUsername());

                            Intent intent = new Intent(Intro.this, MapViewActivity.class);
                            intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_NO_HISTORY);
                            startActivity(intent);
                            finish();

                        } else {
                            if(result.getResult().contains("로그인 에러")){
                                startActivity(new Intent(Intro.this, LoginActivity.class));
                                finish();
                            } else {
                                Utils.showAlert(Intro.this,result.getResult(),result.getMessage());
                            }
                        }
                    }

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

                    }
                });

    } else {
        startActivity(new Intent(getApplication(), LoginActivity.class));
        finish();
    }

}


fun AutoLoginProgress() { // 자동 로그인 체크 검사
     //Log.e("AutoLogin", "Auto Login Stage.");
     pref = getSharedPreferences("pref", Activity.MODE_PRIVATE)
     userID = pref?.getString("userid", "")
     userPW = pref?.getString("userpw", "")
     val keyword = Value.encrypt(Value.URLkey())
     val mfoneNO = Value.encrypt(phoneNumber)
     uID = Settings.Secure.getString(applicationContext.contentResolver,Settings.Secure.ANDROID_ID)
     if (userID != null && !userID!!.isEmpty() && userPW != null && !userPW!!.isEmpty()) {
         mloginService = RetrofitAPI.getClient()!!.create(RemoteService::class.java)
         mloginService!!.Login(Value.encrypt(userID), Value.encrypt(userPW),"", keyword, mfoneNO,Value.encrypt(uID))
             ?.enqueue(object : Callback<LoginResult> {
                 override fun onResponse(
                     call: Call<LoginResult>,
                     response: Response<LoginResult>
                 ) {
                     // 네트워크 통신 성공
                     val result = response.body()
                     if (result!!.result.contains("success")) {
                         Log.e("Intor UserName", result.userinfo!!.username)
                         val intent = Intent(this@Intro, MapViewActivity::class.java)
                         intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_NO_HISTORY)
                         startActivity(intent)
                         finish()
                     } else {
                         if (result.result.contains("로그인 에러")) {
                             startActivity(Intent(this@Intro,LoginActivity::class.java))
                             finish()
                         } else {
                             Utils.showAlert(this@Intro,result.result,result.message)
                         }
                     }
                 }

                 override fun onFailure(
                     call: Call<LoginResult?>,
                     t: Throwable
                 ) {
                 }
             })
     } else {
         startActivity(Intent(application,LoginActivity::class.java))
         finish()
     }
 }


블로그 이미지

Link2Me

,
728x90

앱을 만들다보면 앱을 실행하면서 현재 위치 좌표값만 획득하고 싶을 경우가 있다.


AndroidManifest.xml

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
<!-- Android 10 이상 백그라운드 위치 액세스 요청 --> 


앱 build.gradle

dependencies {
    implementation fileTree(dir: "libs", include: ["*.jar"])
    implementation 'androidx.appcompat:appcompat:1.1.0'
    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'

    implementation 'gun0912.ted:tedpermission:2.0.0'
    implementation 'com.google.android.gms:play-services-location:17.0.0'
    implementation 'com.naver.maps:map-sdk:3.7.1' // 네이버 지도 SDK

}


SplashActivity.java

public class SplashActivity extends AppCompatActivity {
    private final String TAG = this.getClass().getSimpleName();
    Context mContext;

    private final int GPS_ENABLE_REQUEST_CODE = 2001;
    private final long UPDATE_INTERVAL_MS = 1000 * 60 * 15;
    private final long FASTEST_UPDATE_INTERVAL_MS = 1000 * 30 ; // 30초 단위로 화면 갱신

    private FusedLocationProviderClient mFusedLocationProviderClient;
    private LocationRequest locationRequest;
    private double longitude, latitude;

    PermissionListener permissionlistener = new PermissionListener() {
        @Override
        public void onPermissionGranted() {
            initView();
        }

        @Override
        public void onPermissionDenied(ArrayList<String> deniedPermissions) {
            Toast.makeText(SplashActivity.this, "권한 허용을 하지 않으면 서비스를 이용할 수 없습니다.", Toast.LENGTH_SHORT).show();
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_splash);
        mContext = SplashActivity.this;
        PrefsHelper.init(getApplicationContext()); // 한번만 실행하면 된다.

        checkPermissions();
    }

    private void checkPermissions() {

        if(Build.VERSION.SDK_INT >= 26){ // 출처를 알 수 없는 앱 설정 화면 띄우기
            PackageManager pm = mContext.getPackageManager();
            if (!pm.canRequestPackageInstalls()){
                startActivity(new Intent(android.provider.Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES,
                        Uri.parse("package:" + getPackageName())));
            }
        }

        if (Build.VERSION.SDK_INT >= 23) { // 마시멜로(안드로이드 6.0) 이상 권한 체크
            TedPermission.with(mContext)
                    .setPermissionListener(permissionlistener)
                    .setRationaleMessage("앱을 이용하기 위해서는 접근 권한이 필요합니다")
                    .setDeniedMessage("앱에서 요구하는 권한설정이 필요합니다...\n [설정] > [권한] 에서 사용으로 활성화해주세요.")
                    .setPermissions(new String[]{
                            android.Manifest.permission.ACCESS_FINE_LOCATION,
                            android.Manifest.permission.ACCESS_COARSE_LOCATION
                            //android.Manifest.permission.ACCESS_BACKGROUND_LOCATION
                            //앱이 Android 10 이상 앱이 백그라운드에 있는 동안 정기 위치 업데이트를 받을 시
                    })
                    .check();

        } else {
            initView();
        }
    }

    private void initView() {
        checkLocationSetting();
    }

    @SuppressLint("MissingPermission")
    private void checkLocationSetting() {
        // https://developer.android.com/training/location/change-location-settings 읽어라.
        // 위치 요청 설정
        locationRequest = LocationRequest.create();
        locationRequest.setInterval(UPDATE_INTERVAL_MS); // 위치가 Update 되는 주기
        locationRequest.setFastestInterval(FASTEST_UPDATE_INTERVAL_MS); // 위치 획득후 업데이트되는 주기
        locationRequest.setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY);

        // 현재 위치 설정 받기
        LocationSettingsRequest.Builder builder = new LocationSettingsRequest.Builder().addLocationRequest(locationRequest);
        SettingsClient settingsClient = LocationServices.getSettingsClient(this);
        settingsClient.checkLocationSettings(builder.build())
                .addOnSuccessListener(this, locationSettingsResponse -> {
                    mFusedLocationProviderClient = LocationServices.getFusedLocationProviderClient(SplashActivity.this);
                    mFusedLocationProviderClient.requestLocationUpdates(locationRequest, locationCallback, null);
                })
                .addOnFailureListener(SplashActivity.this, e -> {
                    int statusCode = ((ApiException) e).getStatusCode();
                    switch (statusCode) {
                        case LocationSettingsStatusCodes.RESOLUTION_REQUIRED:
                            try {
                                ResolvableApiException rae = (ResolvableApiException) e;
                                rae.startResolutionForResult(SplashActivity.this, GPS_ENABLE_REQUEST_CODE);
                            } catch (IntentSender.SendIntentException sie) {
                                Log.e(TAG, "unable to start resolution for result due to " + sie.getLocalizedMessage());
                            }
                            break;
                        case LocationSettingsStatusCodes.SETTINGS_CHANGE_UNAVAILABLE:
                            Log.e(TAG, "Fix in Settings.");
                    }
                });
    }

    @Override
    public void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);

        if (requestCode == GPS_ENABLE_REQUEST_CODE) {
            if (resultCode == RESULT_OK) {
                checkLocationSetting();
            } else {
                finish();
            }
        }
    }

    private LocationCallback locationCallback = new LocationCallback() {
        @Override
        public void onLocationResult(LocationResult locationResult) {
            super.onLocationResult(locationResult);
            longitude = locationResult.getLastLocation().getLongitude();
            latitude = locationResult.getLastLocation().getLatitude();
            mFusedLocationProviderClient.removeLocationUpdates(locationCallback);

            Log.e(TAG, "latitude:"+latitude+" , longitude:"+longitude);
            PrefsHelper.write("latitude", String.valueOf(latitude));
            PrefsHelper.write("longitude", String.valueOf(longitude));

            Intent intent = new Intent(SplashActivity.this, MainActivity.class);
            startActivity(intent);
            finish();
        }

        @Override
        public void onLocationAvailability(LocationAvailability locationAvailability) {
            super.onLocationAvailability(locationAvailability);
            Log.i(TAG, "onLocationAvailability - " + locationAvailability);
        }
    };

}
 


PrefsHelper.java

public class PrefsHelper {
    public static final String PREFERENCE_NAME="pref";
    private Context mContext;
    private static SharedPreferences prefs;
    private static SharedPreferences.Editor prefsEditor;
    private static PrefsHelper instance;

    public static synchronized PrefsHelper init(Context context){
        if(instance == null)
            instance = new PrefsHelper(context);
        return instance;
    }

    private PrefsHelper(Context context) {
        mContext = context;
        prefs = mContext.getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE );
        prefsEditor = prefs.edit();
    }

    public static String read(String key, String defValue) {
        return prefs.getString(key, defValue);
    }

    public static void write(String key, String value) {
        prefsEditor.putString(key, value);
        prefsEditor.commit();
        // double 변수 값은 string으로 감싸서 데이터를 넘기면 된다.
    }

    public static Integer read(String key, int defValue) {
        return prefs.getInt(key, defValue); // 32비트
    }

    public static void write(String key, Integer value) {
        prefsEditor.putInt(key, value).commit();
    }

    public static long read(String key, Long defValue) {
        return prefs.getLong(key, defValue); // 64비트
    }

    public static void write(String key, Long value) {
        prefsEditor.putLong(key, value).commit();
    }

    public static boolean read(String key, boolean defValue) {
        return prefs.getBoolean(key, defValue);
    }

    public static void write(String key, boolean value) {
        prefsEditor.putBoolean(key, value);
        prefsEditor.commit();
    }
}


지도를 띄우지 않은 상태에서 현재 위치 좌표만 얻어지는 걸 확인할 수 있다.


참고하면 좋은 게시글

https://medium.com/@droidbyme/android-turn-on-gps-programmatically-d585cf29c1ef



블로그 이미지

Link2Me

,
728x90

RecyclerviewAdapter 에서 Interface로 Activity 로 값을 넘기는 방법에 대한 코드와 Checkbox 에서 전체 선택/해제를 위해 별도의 RelativeLayout 으로 전체선택, 취소, 전송 등의 버튼을 처리하도록 한다.

구현은 전체를 다 구현한 것이 아니라 핵심사항만 구현했지만, 이 정도로도 좋은 코드라고 본다.


MainActivity.java

public class MainActivity extends AppCompatActivity implements RecyclerViewAdapter.OnItemClickListener {
    private final String TAG = this.getClass().getSimpleName();
    Context context;

    public ArrayList<Address_Item> addressItemList = new ArrayList<>(); // 서버 원본 데이터 리스트
    private RecyclerView mRecyclerView;
    private RecyclerViewAdapter mAdapter;
    private SearchView editsearch;

    RemoteService remoteService;

    public static RelativeLayout relative2;
    public static boolean isCheckFlag = false;
    public CheckBox checkAll;

    private BackPressHandler backPressHandler;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        context = MainActivity.this;
        backPressHandler = new BackPressHandler(this);

        initView();
    }

    private void initView() {
        createAddressList(); // 서버 데이터 가져오기
        buildRecyclerView();
        setButtons();
    }

    private void setButtons(){
        isCheckFlag = false;
        // Relative layout 정의
        relative2 = findViewById(R.id.list_view_relative2);

        if (isCheckFlag == false) {
            relative2.setVisibility(View.GONE);
        } else if (isCheckFlag == true) {
            relative2.setVisibility(View.VISIBLE);
        }

        // all checkbox
        checkAll = findViewById(R.id.lv_checkbox_all);
        checkAll.setOnCheckedChangeListener((buttonView, isChecked) -> {
            if (checkAll.isChecked() == true) {
                mAdapter.selectAll();
                mAdapter.notifyDataSetChanged();
            } else {
                mAdapter.unselectall();
                mAdapter.notifyDataSetChanged();
            }
        });

        Button cancel = findViewById(R.id.btn_cancel);
        cancel.setOnClickListener(v -> {
            isCheckFlag = false;
            relative2.setVisibility(View.GONE);
            mAdapter.unselectall();
            checkAll.setChecked(false); // 전체 선택 체크박스 해제
            mAdapter.notifyDataSetChanged();
        });

        Button send = findViewById(R.id.btn_send);
        final String[] listview_items = {"그룹문자 보내기", "연락처 저장"};
        final AlertDialog.Builder items_builder = new AlertDialog.Builder(MainActivity.this);
        items_builder.setTitle("해당 작업을 선택하세요");
        items_builder.setItems(listview_items, new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                switch (which) {
                    case 0:
                        Toast.makeText(getApplicationContext(), "그룹문자 보내기 구현하세요.", Toast.LENGTH_SHORT).show();
                        break;
                    case 1:
                        Toast.makeText(getApplicationContext(), "연락처 저장을 구현하세요.", Toast.LENGTH_SHORT).show();
                        break;
                }
            }
        });
        items_builder.create();

        send.setOnClickListener(v ->

                items_builder.show());

    }

    private void buildRecyclerView(){
        mRecyclerView = findViewById(R.id.address_listview);
        mRecyclerView.setHasFixedSize(true);
        LinearLayoutManager manager = new LinearLayoutManager(context);
        mAdapter = new RecyclerViewAdapter(context,addressItemList);

        DividerItemDecoration decoration = new DividerItemDecoration(context,manager.getOrientation());
        mRecyclerView.addItemDecoration(decoration);
        mRecyclerView.setLayoutManager(manager);
        mRecyclerView.setAdapter(mAdapter);

        mAdapter.setOnItemSelectClickListener(this);
    }

    private void createAddressList() {
         // 서버 데이터 가져오기 구현 메서드는 https://link2me.tistory.com/1850 참조
    }

    @Override
    public void onBackPressed() {
        backPressHandler.onBackPressed();
    }

    @Override
    public void onItemClicked(View view, Address_Item item, int position) {
        Toast.makeText(context, "position : "+position + " clieck item name : "+item.getUserNM(), Toast.LENGTH_SHORT).show();
    }
}


RecyclerViewAdapter.java

public class RecyclerViewAdapter extends RecyclerView.Adapter<RecyclerViewAdapter.ViewHolder> {
    private final String TAG = this.getClass().getSimpleName();
    Context context;
    private List<Address_Item> rvItemList;

    // 인터페이스 선언 -------------------------------------------------------------
    private OnItemClickListener mListener;

    public interface OnItemClickListener {
        void onItemClicked(View view, Address_Item item, int position);
    }

    public void setOnItemSelectClickListener(OnItemClickListener listener){
        mListener = listener;
    }
    // 인터페이스 ----------------------------------------------------------------

    public class ViewHolder extends RecyclerView.ViewHolder{
        LinearLayout child_layout;
        ImageView photo_Image;
        TextView tv_name;
        TextView tv_mobileNO;
        TextView tv_officeNO;
        ImageView call_btn;
        CheckBox cbSelect;

        public ViewHolder(@NonNull View itemView) {
            super(itemView);
            child_layout = itemView.findViewById(R.id.child_layout);
            photo_Image = itemView.findViewById(R.id.profile_Image);
            tv_name = itemView.findViewById(R.id.child_name);
            tv_mobileNO = itemView.findViewById(R.id.child_mobileNO);
            tv_officeNO = itemView.findViewById(R.id.child_officeNO);
            call_btn = itemView.findViewById(R.id.call_btn);
            cbSelect = itemView.findViewById(R.id.listcell_checkbox);
        }
    }

    public RecyclerViewAdapter(Context context, List<Address_Item> itemList) {
        this.context = context;
        rvItemList = itemList;
    }

    public void selectAll(){ // checkbox 전체 선택
        for(int i=0;i < rvItemList.size();i++){
            rvItemList.get(i).setCheckBoxState(true);
        }
        notifyDataSetChanged();
    }
    public void unselectall(){ // checkbox 전체 해제
        for(int i=0;i < rvItemList.size();i++){
            rvItemList.get(i).setCheckBoxState(false);
        }
        notifyDataSetChanged();
    }

    @NonNull
    @Override
    public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.address_item,parent,false);
        ViewHolder holder = new ViewHolder(view);
        return holder;
    }

    @Override
    public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
        Log.d(TAG,"onBindViewHolder: called."); // 실제로 데이터를 표시하는 부분
        Address_Item currentItem = rvItemList.get(position);

        String photoURL = Value.IPADDRESS + "photos/" + currentItem.getPhoto();
        if(photoURL.contains("null")){
            Glide.with(context).asBitmap().load(R.drawable.photo_base).into(holder.photo_Image);
        } else {
            Glide.with(context).asBitmap().load(photoURL).into(holder.photo_Image);
        }
        holder.tv_name.setText(currentItem.getUserNM());
        holder.tv_mobileNO.setText(PhoneNumberUtils.formatNumber(currentItem.getMobileNO()));
        holder.tv_officeNO.setText(PhoneNumberUtils.formatNumber(currentItem.getTelNO()));

        holder.photo_Image.setOnClickListener(view1 -> {
            if(mListener != null){
                mListener.onItemClicked(view1,currentItem,position);
            }
        });

        holder.call_btn.setOnClickListener(view -> {
            Toast.makeText(context, "전화걸기 또는 문자 보내기를 할 수 있어요.", Toast.LENGTH_SHORT).show();
        });

        if (isCheckFlag == false) {
            holder.call_btn.setVisibility(View.VISIBLE);
            holder.call_btn.setOnClickListener(view -> builder.show());
            holder.cbSelect.setVisibility(View.GONE);
            holder.child_layout.setOnClickListener(view ->
                    Toast.makeText(context, "상세보기를 눌렀습니다 ===" + currentItem.getUid(), Toast.LENGTH_SHORT).show());

            holder.child_layout.setOnLongClickListener(v -> {
                isCheckFlag = true;
                relative2.setVisibility(View.VISIBLE);
                notifyDataSetChanged();
                return true;
            });
        } else {
            holder.call_btn.setVisibility(View.GONE);
            holder.cbSelect.setVisibility(View.VISIBLE);
            holder.cbSelect.setTag(position); // This line is important.

            // 체크 박스 클릭하면 CheckBoxState 에 반영한다. setOnCheckedChangeListener 대신 사용
            holder.cbSelect.setOnClickListener(v -> {
                if(rvItemList.get(position).getCheckBoxState() == true){
                    rvItemList.get(position).setCheckBoxState(false);
                } else {
                    rvItemList.get(position).setCheckBoxState(true);
                }
            });

            holder.child_layout.setOnClickListener(v -> {
                if(holder.cbSelect.isChecked() == false){
                    holder.cbSelect.setChecked(true);
                    rvItemList.get(position).setCheckBoxState(true);
                } else {
                    holder.cbSelect.setChecked(false);
                    rvItemList.get(position).setCheckBoxState(false);
                }
            });
        }

        // 재사용 문제 해결
        if(rvItemList.get(position).getCheckBoxState() == true){
            holder.cbSelect.setChecked(true);          
        } else {
            holder.cbSelect.setChecked(false);          
        }

    }

    @Override
    public int getItemCount() {
        return rvItemList.size();
    }

}


Address_Item.kt

import android.os.Parcelable
import kotlinx.android.parcel.Parcelize

@Parcelize
data class Address_Item (
    // 결과를 받을 모델 (ArrayList 에 저장하므로 val 로 선언하면 안된다)
    var uid: String="",
    var userNM: String="",
    var mobileNO: String?=null,
    var telNO: String?=null,
    var photo: String?=null,
    var checkBoxState: Boolean
): Parcelable


address_item.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_marginBottom="1dp"
    app:cardCornerRadius="1dp">

    <LinearLayout
        android:id="@+id/child_layout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        android:padding="10dp"
        android:paddingLeft="10dp"
        android:paddingRight="10dp">

        <ImageView
            android:id="@+id/profile_Image"
            android:layout_width="80dp"
            android:layout_height="80dp"
            android:layout_gravity="center"
            android:layout_weight="1"
            android:src="@mipmap/ic_launcher" />

        <LinearLayout
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginLeft="10dp"
            android:layout_weight="4"
            android:orientation="vertical">

            <TextView
                android:id="@+id/child_name"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:paddingBottom="2dip"
                android:text="이름"
                android:textSize="20sp"
                android:textStyle="bold" />

            <TextView
                android:id="@+id/child_mobileNO"
                android:layout_width="fill_parent"
                android:layout_height="wrap_content"
                android:gravity="left"
                android:text="휴대폰번호"
                android:textStyle="bold" />

            <TextView
                android:id="@+id/child_officeNO"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:gravity="left"
                android:text="사무실번호"
                android:textStyle="bold" />

        </LinearLayout>

        <LinearLayout
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:layout_marginLeft="5dp"
            android:layout_weight="1"
            android:orientation="vertical">

            <ImageView
                android:id="@+id/call_btn"
                android:layout_width="40dp"
                android:layout_height="40dp"
                android:layout_gravity="center"
                android:gravity="center_horizontal|center_vertical"
                android:src="@drawable/btn_phone" />

        </LinearLayout>

        <CheckBox
            android:id="@+id/listcell_checkbox"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:focusable="false" />

    </LinearLayout>
</androidx.cardview.widget.CardView>



블로그 이미지

Link2Me

,
728x90

메인 화면 구성을 위해서 구글에서 Github 검색으로 https://github.com/Whhoesj/sliding-content-drawer 를 하나 찾아서 테스트하고 간단하게 적어둔다.

 


화면 구성은 위와 같다.

https://link2me.tistory.com/1810 에 Navigation Drawer 구성에 대한 개념이 설명되어 있는데, SlidingContentDrawer 는 아래 Java 코드만 살펴보면 이해될 것이다.


Project build.gradle

allprojects {
    repositories {
        google()
        jcenter()
        maven { url 'https://navercorp.bintray.com/maps' }
        maven { url "https://jitpack.io" }
    }
}


앱 build.gradle

dependencies {
    implementation fileTree(dir: "libs", include: ["*.jar"])
    implementation 'androidx.appcompat:appcompat:1.1.0'
    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'

    implementation 'gun0912.ted:tedpermission:2.0.0'
    implementation 'com.google.android.material:material:1.1.0'
    implementation 'com.wouterhabets:slidingcontentdrawer:1.0'
    implementation 'com.naver.maps:map-sdk:3.7.1' // 네이버 지도 SDK
    implementation 'com.google.android.gms:play-services-location:17.0.0'
}


AndroidManifest.xml

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

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

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="false"
        android:theme="@style/AppTheme.NoActionBar">
        <activity android:name=".SplashActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <activity android:name=".MapActivity" />
        <activity android:name=".MainActivity" />

        <meta-data
            android:name="com.naver.maps.map.CLIENT_ID"
            android:value="@string/naver_app_key" />
    </application>

</manifest>


strings.xml

<resources>
    <string name="app_name">SlidingContentDrawer</string>
    <string name="navigation_drawer_open">Open navigation drawer</string>
    <string name="navigation_drawer_close">Close navigation drawer</string>
    <string name="naver_app_key">여기에 키를 적어주세요</string>
</resources>


Drawer Layout 구성

- Layout 구성을 버전에 맞춰 변경 수정했다.

- 파일을 3개의 파일로 분리 구성했다.

<?xml version="1.0" encoding="utf-8"?>
<com.wouterhabets.slidingcontentdrawer.widget.SlidingDrawerLayout
    android:id="@+id/drawer_layout"
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="?attr/colorPrimaryDark"
    android:fitsSystemWindows="true">

    <ImageView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:scaleType="centerCrop"
        android:src="@drawable/background"/>

    <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:tag="menu">

        <include layout="@layout/drawer_menu"/>

    </FrameLayout>

    <androidx.coordinatorlayout.widget.CoordinatorLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="?android:colorBackground"
        android:elevation="6dp"
        android:fitsSystemWindows="true"
        android:tag="content">

        <View
            android:layout_width="match_parent"
            android:layout_height="10dp"
            android:background="@android:color/holo_green_light"/>

        <com.google.android.material.appbar.AppBarLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:theme="@style/AppTheme.AppBarOverlay">

            <androidx.appcompat.widget.Toolbar
                android:id="@+id/toolbar"
                android:layout_width="match_parent"
                android:layout_height="?attr/actionBarSize"
                android:background="?attr/colorPrimary"
                app:popupTheme="@style/AppTheme.PopupOverlay"/>

        </com.google.android.material.appbar.AppBarLayout>

        <include
            layout="@layout/drawer_content"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />

        <com.google.android.material.floatingactionbutton.FloatingActionButton
            android:id="@+id/fab"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="bottom|end"
            android:layout_margin="@dimen/fab_margin"
            android:src="@android:drawable/ic_dialog_email"/>

    </androidx.coordinatorlayout.widget.CoordinatorLayout>

</com.wouterhabets.slidingcontentdrawer.widget.SlidingDrawerLayout>

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_gravity="start|center_vertical"
    android:orientation="vertical"
    android:paddingLeft="16dp">

    <TextView
        android:id="@+id/navermap_to"
        android:layout_width="match_parent"
        android:layout_height="42dp"
        android:layout_gravity="start|center_vertical"
        android:text="네이버 지도"
        android:layout_marginBottom="30dp"
        android:textColor="@android:color/white"/>

    <TextView
        android:layout_width="match_parent"
        android:layout_height="42dp"
        android:layout_gravity="start|center_vertical"
        android:text="Menu item 2"
        android:layout_marginBottom="30dp"
        android:textColor="@android:color/white"/>

    <TextView
        android:layout_width="match_parent"
        android:layout_height="42dp"
        android:layout_gravity="start|center_vertical"
        android:text="Menu item 3"
        android:layout_marginBottom="30dp"
        android:textColor="@android:color/white"/>

    <TextView
        android:layout_width="match_parent"
        android:layout_height="42dp"
        android:layout_gravity="start|center_vertical"
        android:text="Menu item 4"
        android:layout_marginBottom="30dp"
        android:textColor="@android:color/white"/>

    <TextView
        android:layout_width="match_parent"
        android:layout_height="42dp"
        android:layout_gravity="start|center_vertical"
        android:text="Menu item 5"
        android:textColor="@android:color/white"/>
</LinearLayout>

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent"
    android:layout_height="match_parent">

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:paddingBottom="@dimen/activity_vertical_margin"
        android:paddingLeft="@dimen/activity_horizontal_margin"
        android:paddingRight="@dimen/activity_horizontal_margin"
        android:paddingTop="@dimen/activity_vertical_margin">

        <TextView
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:gravity="center"
            android:text="@string/app_name"/>
    </RelativeLayout>

</androidx.constraintlayout.widget.ConstraintLayout>



MainActivity.java 구현

package com.link2me.android.slidingcontentdrawer;

import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;

import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.TextView;

import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.google.android.material.snackbar.Snackbar;
import com.link2me.android.common.BackPressHandler;
import com.link2me.android.common.PrefsHelper;
import com.wouterhabets.slidingcontentdrawer.widget.SlidingDrawerLayout;
import com.wouterhabets.slidingcontentdrawer.widget.SlidingDrawerToggle;

public class MainActivity extends AppCompatActivity implements View.OnClickListener {
    private final String TAG = this.getClass().getSimpleName();
    Context mContext;
    private BackPressHandler backPressHandler;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mContext = MainActivity.this;
        backPressHandler = new BackPressHandler(this); // 뒤로 가기 버튼 이벤트

        initView();
    }

    private void initView() {
        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);

        FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
        fab.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG)
                        .setAction("Action", null).show();
            }
        });

        SlidingDrawerLayout drawer = (SlidingDrawerLayout) findViewById(R.id.drawer_layout);
        SlidingDrawerToggle toggle = new SlidingDrawerToggle(
                this, drawer, toolbar, R.string.navigation_drawer_open, R.string.navigation_drawer_close);
        drawer.setDrawerListener(toggle);
        toggle.syncState();

        // 메뉴 버튼 클릭 리스너
        findViewById(R.id.navermap_to).setOnClickListener(this);
    }

    @Override
    public void onBackPressed() {
        SlidingDrawerLayout drawer = (SlidingDrawerLayout) findViewById(R.id.drawer_layout);
        if (drawer.isDrawerOpen()) {
            drawer.closeDrawer();
        } else {
            backPressHandler.onBackPressed();
        }
    }

    @Override
    public void onClick(View view) {
        switch (view.getId()){
            case R.id.navermap_to:
                Intent intent = new Intent(mContext,MapActivity.class);
                startActivity(intent);
                break;
        }
    }
}


MapActivity.java 구현

package com.link2me.android.slidingcontentdrawer;

import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.cardview.widget.CardView;
import androidx.drawerlayout.widget.DrawerLayout;

import android.content.Context;
import android.os.Bundle;
import android.view.View;
import android.widget.ImageView;

import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.google.android.material.snackbar.Snackbar;
import com.link2me.android.common.PrefsHelper;
import com.naver.maps.geometry.LatLng;
import com.naver.maps.map.CameraAnimation;
import com.naver.maps.map.CameraUpdate;
import com.naver.maps.map.LocationTrackingMode;
import com.naver.maps.map.MapView;
import com.naver.maps.map.NaverMap;
import com.naver.maps.map.OnMapReadyCallback;
import com.naver.maps.map.UiSettings;
import com.naver.maps.map.overlay.InfoWindow;
import com.naver.maps.map.util.FusedLocationSource;
import com.naver.maps.map.widget.LocationButtonView;

public class MapActivity extends AppCompatActivity implements OnMapReadyCallback {
    private final String TAG = this.getClass().getSimpleName();
    Context mContext;

    // NaverMap API 3.0
    private MapView mMapView;
    private LocationButtonView locBtnView;
    DrawerLayout drawer_layout;
    ImageView btn_drawer;
    CardView cardView;

    // FusedLocationSource (Google)
    private static final int LOCATION_PERMISSION_REQUEST_CODE = 1000;
    private FusedLocationSource locationSource;

    private boolean  isFirstLocation = true;
    private NaverMap mNaverMap;
    private InfoWindow mInfoWindow; // 지도 위에 올리는 정보 창을 정의
    private boolean showMarket = false;

    String latitude;
    String longitude;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_map);
        mContext = MapActivity.this;

        // https://console.ncloud.com/mc/solution/naverService/application 에서 패키지명 등록 확인부터하라.
        mMapView = findViewById(R.id.map_view);
        mMapView.onCreate(savedInstanceState);
        mMapView.getMapAsync(this);

        // 내 현재 위치 버튼
        locBtnView = findViewById(R.id.btn_curr_location);
        // implementation "com.google.android.gms:play-services-location:17.0.0" 안해주면 에러 발생
        // 내 위치 찾기 위한 소스
        locationSource = new FusedLocationSource(this,LOCATION_PERMISSION_REQUEST_CODE);

        FloatingActionButton fab_message = findViewById(R.id.fab_message);
        fab_message.setOnClickListener(view -> {
            Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG)
                    .setAction("Action", null).show();
        });
    }

    @Override
    public void onMapReady(@NonNull NaverMap naverMap) {
        UiSettings uiSettings = naverMap.getUiSettings();
        uiSettings.setLocationButtonEnabled(false);
        uiSettings.setCompassEnabled(true);

        // 카메라 초기 위치 설정
        LatLng initialPosition = new LatLng(37.506855, 127.066242);

        mNaverMap = naverMap;

        locBtnView.setMap(naverMap);

        // Location Change Listener를 사용하기 위한 FusedLocationSource 설정
        naverMap.setLocationSource(locationSource);
        naverMap.setLocationTrackingMode(LocationTrackingMode.NoFollow); // LocationTrackingMode.Face

        naverMap.addOnLocationChangeListener(location -> {
            if (isFirstLocation) { // 현재 위치로 초기 화면 이동
                naverMap.setLocationTrackingMode(LocationTrackingMode.None);
                LatLng initialPosition1 = new LatLng(location.getLatitude(),location.getLongitude()); // 현재 위치
                latitude = String.valueOf(location.getLatitude());
                longitude = String.valueOf(location.getLongitude());
                PrefsHelper.write("latitude",latitude);
                PrefsHelper.write("longitude",longitude);
                //Log.e(TAG,"latitude:"+latitude+" , longitude:"+longitude);
                CameraUpdate cameraUpdate = CameraUpdate.scrollTo(initialPosition1).animate(CameraAnimation.Easing);
                naverMap.moveCamera(cameraUpdate);
            }
            isFirstLocation = false;
        });

    }
}


지도 처리를 위해서는 Naver Cloud 에 앱패키지명을 등록해야 한다.


테스트에 사용한 파일을 첨부한다.

slidingcontentdrawer.zip


블로그 이미지

Link2Me

,
728x90

작성일자 : 2020.7.14


Retrofit2 라이브러리를 이용하여 데이터 처리하는 걸 테스트하다가 완전 삽질을 한참 했다.

Array로 전체 데이터를 받아서 처리해야 하는데 그렇게 처리하지 않아서 헤메고 있었다.


Retrofit2 기본 개념은 https://link2me.tistory.com/1806 잘 정리되어 있다.


public interface DataCommAPI {
    // POST 방식으로 데이터를 주고 받을 때 넘기는 변수는 Field 라고 해야 한다.
    @FormUrlEncoded
    @POST(Value.AddressData)
    Call<List<Address_Item>> GetAddrData(
            @Field("keyword") String keyword,
            @Field("search") String search
    );
}

@Parcelize
data class Address_Item (
    // 결과를 받을 모델
    var uid: String="",
    var userNM: String="",
    var mobileNO: String?=null,
    var telNO: String?=null,
    var photo: String?=null,
    var checkBoxState: Boolean
): Parcelable


MainActivity.java

public class MainActivity extends AppCompatActivity {
    private final String TAG = this.getClass().getSimpleName();
    Context context;

    private ArrayList<Address_Item> addressItemList = new ArrayList<>(); // 서버에서 가져온 원본 데이터 리스트
    private ArrayList<Address_Item> searchItemList = new ArrayList<>(); // 검색한 데이터 리스트
    private RecyclerView mRecyclerView;
    private RecyclerViewAdapter mAdapter;
    private SearchView editsearch;

    DataCommAPI dataCommAPI;

    static boolean isMSG = false;
    static boolean isAll = false;
    static boolean flag = false;
    static CheckBox checkAll;

    RelativeLayout relative2;
    private BackPressHandler backPressHandler;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        context = MainActivity.this;
        backPressHandler = new BackPressHandler(this);

        initView();
    }

    private void initView() {
        isMSG = false;
        isAll = false;
        // Relative layout 정의
        relative2 = (RelativeLayout) findViewById(R.id.list_view_relative2);

        mRecyclerView = findViewById(R.id.address_listview);
        mRecyclerView.setHasFixedSize(true);
        LinearLayoutManager manager = new LinearLayoutManager(context);
        DividerItemDecoration decoration = new DividerItemDecoration(context,manager.getOrientation());
        mRecyclerView.addItemDecoration(decoration);
        mRecyclerView.setLayoutManager(manager);

        mAdapter = new RecyclerViewAdapter(context,addressItemList);
        mRecyclerView.setAdapter(mAdapter);

        // Adapter에 추가 데이터를 저장하기 위한 ArrayList
        getServerData(); // 서버 데이터 가져오기
    }

    private void getServerData() {
        Retrofit retrofit = new Retrofit.Builder()
                .baseUrl(Value.IPADDRESS)
                .addConverterFactory(GsonConverterFactory.create())
                .client(createOkHttpClient())
                .build();

        dataCommAPI = retrofit.create(DataCommAPI.class);

        String keyword = Value.encrypt(Value.URLkey());
        dataCommAPI.GetAddrData(keyword,"").enqueue(new Callback<List<Address_Item>>() {
            @Override
            public void onResponse(Call<List<Address_Item>> call, Response<List<Address_Item>> response) {
                List<Address_Item> result = response.body(); // 전체 데이터

                addressItemList.clear(); // 서버에서 가져온 데이터 초기화
                searchItemList.clear();
               
                for(Address_Item item : result){
                    addressItemList.add(item);
                    searchItemList.add(item);
                }
                runOnUiThread(new Runnable() { // runOnUiThread()를 호출하여 실시간 갱신한다.
                    @Override
                    public void run() {
                        // 갱신된 데이터 내역을 어댑터에 알려줌
                        mAdapter.notifyDataSetChanged();
                    }
                });
            }

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

            }
        });

    }

    private OkHttpClient createOkHttpClient() {
        // 네트워크 통신 로그(서버로 보내는 파라미터 및 받는 파라미터) 보기
        OkHttpClient.Builder builder = new OkHttpClient.Builder();
        HttpLoggingInterceptor interceptor = new HttpLoggingInterceptor();
        interceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
        builder.addInterceptor(interceptor);
        return builder.build();
    }

    public void showAlert(String title, String message) {
        AlertDialog.Builder builder = new AlertDialog.Builder(context);
        builder.setTitle(title);
        builder.setMessage(message)
                .setCancelable(false)
                .setPositiveButton("OK", new DialogInterface.OnClickListener() {
                    public void onClick(DialogInterface dialog, int id) {
                        dialog.dismiss();
                    }
                });
        AlertDialog alert = builder.create();
        alert.show();
    }

    @Override
    public void onBackPressed() {
        backPressHandler.onBackPressed();
    }
}



통신상태가 성공, 실패 여부를 먼저 확인한 다음에

ArrayList 데이터 전체를 출력하는 걸 처리하는 사항

@Parcelize
class AddressResult (
    val status: String,
    val message: String="",
    val addrinfo: List<Address_Item>? = null

): Parcelable
 

public interface RemoteService {
    // POST 방식으로 데이터를 주고 받을 때 넘기는 변수는 Field 라고 해야 한다.
    @FormUrlEncoded
    @POST(Value.AddressData)
    Call<AddressResult> GetAddrData(
            @Field("keyword") String keyword,
            @Field("search") String search
    );
}

private void getServerData() {
    Retrofit retrofit = new Retrofit.Builder()
            .baseUrl(Value.IPADDRESS)
            .addConverterFactory(GsonConverterFactory.create())
            .client(createOkHttpClient())
            .build();

    dataCommAPI = retrofit.create(RemoteService.class);

    String keyword = Value.encrypt(Value.URLkey());
    dataCommAPI.GetAddrData(keyword,"").enqueue(new Callback<AddressResult>() {
        @Override
        public void onResponse(Call<AddressResult> call, Response<AddressResult> response) {
            if(response.body().getStatus().contains("success")){
                addressItemList.clear(); // 서버에서 가져온 데이터 초기화
                searchItemList.clear();

                for(Address_Item item : response.body().getAddrinfo()){
                    addressItemList.add(item);
                    searchItemList.add(item);
                }
                runOnUiThread(new Runnable() { // runOnUiThread()를 호출하여 실시간 갱신한다.
                    @Override
                    public void run() {
                        // 갱신된 데이터 내역을 어댑터에 알려줌
                        mAdapter.notifyDataSetChanged();
                    }
                });
            } else {
                showAlert(response.body().getStatus(),response.body().getMessage());
            }
        }

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

        }
    });

}


추가 오픈일자 : 2020.9.15일

위 안드로이드 코드와 관련된 PHP 코드

<?php
// 파일을 직접 실행하면 동작되지 않도록 하기 위해서
if(isset($_POST) && $_SERVER['REQUEST_METHOD'] == "POST"){
    @extract($_POST); // POST 전송으로 전달받은 값 처리
    require_once 'config/config.php';
    require_once 'phpclass/dbconnect.php';
    require_once 'phpclass/loginClass.php';
    $c = new LoginClass;

    header("Cache-Control: no-cache, must-revalidate");
    header("Content-type: application/json; charset=UTF-8");

    $R = array(); // 결과 담을 변수 생성
    // 화면에 출력할 칼럼 발췌
    $sql = "select uid,userNM,mobileNO,telNO,photo from Person ";
    if(!empty($search)) {
        $sql .= "where userNM LIKE '%".$search."%' or mobileNO LIKE '%".$search."%'";
    }
    $result = mysqli_query($db,$sql);
    while($row = mysqli_fetch_array($result)) {
        $photo ="null";
        $photo_path = './photos/'.$row['photo'];
        if(file_exists($photo_path)) {
            $photo =$row['photo'];
        }
        array_push($R,  array("uid"=>$row['uid'],"userNM"=>$row['userNM'],"mobileNO"=>$row['mobileNO'],

                                        "telNO"=>$row['telNO'],"photo"=>$photo,"checkBoxState"=>false));
    }

    $status = "success";
    $message = "";
    $addrinfo = $R; // 전체 ArrayList 데이터

    $result = array(
        'status' => $status,
        'message' => $message,
        'addrinfo' => $addrinfo
    );
    echo json_encode($result);

}
?>


블로그 이미지

Link2Me

,
728x90

이전 코드 구현에서 Gson 라이브러리를 사용하여 코드를 변경하는 부분이 핵심이다.


package com.link2me.android.recyclerviewjava.model

import android.os.Parcelable
import kotlinx.android.parcel.Parcelize

@Parcelize
data class Address_Item (
    // 결과를 받을 모델, 코틀린 혼용하다보니 코틀린의 문법 적용을 받게되어 null 처리를 조심해야 하더라.
    var uid: String="",
    var userNM: String="",
    var mobileNO: String?=null,
    var telNO: String?=null,
    var photo: String?=null,
    var checkBoxState: Boolean
): Parcelable


package com.link2me.android.recyclerviewjava;

import android.content.Context;
import android.content.DialogInterface;
import android.os.Bundle;
import android.util.Log;
import android.widget.CheckBox;
import android.widget.RelativeLayout;

import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.SearchView;
import androidx.recyclerview.widget.DividerItemDecoration;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;

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.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import com.link2me.android.recyclerviewjava.adapter.RecyclerViewAdapter;
import com.link2me.android.recyclerviewjava.model.Address_Item;
import com.link2me.android.recyclerviewjava.model.DataCommAPI;

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

import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.HashMap;

public class MainActivity extends AppCompatActivity {
    private final String TAG = this.getClass().getSimpleName();
    Context context;

    private ArrayList<Address_Item> addressItemList = new ArrayList<>(); // 서버 원본 데이터 리스트
    private ArrayList<Address_Item> searchItemList = new ArrayList<>(); // 검색한 데이터 리스트
    private RecyclerView mRecyclerView;
    private RecyclerViewAdapter mAdapter;
    private SearchView editsearch;

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

        initView();
    }

    private void initView() {
        // Adapter에 추가 데이터를 저장하기 위한 ArrayList
        getServerData(Value.IPADDRESS+"getJSONData.php"); // 서버 데이터 가져오기
    }

    private void getServerData(String SERVER_URL) {
        HashMap<String, String> params = new HashMap<>();
        params.put("keyword", Value.encrypt(Value.URLkey()));
        JSONObject jsonObject = new JSONObject(params);

        // 1. RequestQueue 생성 및 초기화
        RequestQueue requestQueue = Volley.newRequestQueue(context);

        // 2. Request Obejct인 StringRequest 생성
        JsonObjectRequest request = new JsonObjectRequest(Request.Method.POST, SERVER_URL,jsonObject,
                (Response.Listener<JSONObject>) response -> {
                    Log.e("result", "" + response);
                    try {
                        if(response.getString("status").equals("success")){
                            String jsondata = response.getString("addrinfo");
                            showJSONList(jsondata);
                        } else {
                            showAlert(response.getString("status"),response.getString("message"));
                        }
                    } catch (JSONException e) {
                        e.printStackTrace();
                    }
                },
                error -> Log.d("error", "[" + error.getMessage() + "]")) {

        };

        // 3) 생성한 StringRequest를 RequestQueue에 추가
        requestQueue.add(request);
    }

    private void showJSONList(String JSONdata) {
        try {
            JSONArray peoples = new JSONArray(JSONdata); // [] 대괄호
            addressItemList.clear(); // 서버에서 가져온 데이터 초기화

            Gson gson = new Gson();
            Type listType = new TypeToken<ArrayList<Address_Item>>(){}.getType();
            addressItemList = gson.fromJson(peoples.toString(),listType);
            searchItemList = gson.fromJson(peoples.toString(),listType);

            mRecyclerView = findViewById(R.id.address_listview);
            mAdapter = new RecyclerViewAdapter(context,addressItemList);
            LinearLayoutManager manager = new LinearLayoutManager(context);
            DividerItemDecoration decoration = new DividerItemDecoration(context,manager.getOrientation());

            mRecyclerView.addItemDecoration(decoration);
            mRecyclerView.setLayoutManager(manager);
            mRecyclerView.setAdapter(mAdapter);


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

    public void showAlert(String title, String message) {
        AlertDialog.Builder builder = new AlertDialog.Builder(context);
        builder.setTitle(title);
        builder.setMessage(message)
                .setCancelable(false)
                .setPositiveButton("OK", new DialogInterface.OnClickListener() {
                    public void onClick(DialogInterface dialog, int id) {
                        dialog.dismiss();
                    }
                });
        AlertDialog alert = builder.create();
        alert.show();
    }

    @Override
    public void onBackPressed() {
        backPressHandler.onBackPressed();
    }
}


블로그 이미지

Link2Me

,
728x90

오랫만에 서버에 있는 자료를 가져오는 걸 했는데 삽질을 한참이나 했다.

코틀린으로의 변환을 고려하여 앱 build.gradle은 아래와 같이 작성했다.


앱 build.gradle

apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'

android {
    compileSdkVersion 30
    buildToolsVersion "30.0.0"

    defaultConfig {
        applicationId "com.link2me.android.recyclerviewjava"
        minSdkVersion 23
        targetSdkVersion 30
        versionCode 1
        versionName "1.0"
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }

    compileOptions {
        sourceCompatibility = 1.8
        targetCompatibility = 1.8
    }

}

dependencies {
    implementation fileTree(dir: "libs", include: ["*.jar"])
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
    implementation 'androidx.appcompat:appcompat:1.1.0'
    implementation 'androidx.core:core-ktx:1.3.0'
    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'

    implementation 'com.google.android.material:material:1.1.0'
    implementation 'androidx.cardview:cardview:1.0.0'  // 레이아웃으로 사용할 CardView
    implementation 'androidx.recyclerview:recyclerview:1.1.0'

    implementation 'com.android.volley:volley:1.1.1'
    implementation 'com.google.code.gson:gson:2.8.6'

    // 이미지 출력용 Glide
    implementation 'com.github.bumptech.glide:glide:4.11.0'
    annotationProcessor 'com.github.bumptech.glide:compiler:4.11.0'

}
 


모델 작성

- 코틀린으로 코드를 구현했다. Java 와 코틀린 혼용 코딩이 가능하다.

package com.link2me.android.recyclerviewjava.model

import android.os.Parcelable
import kotlinx.android.parcel.Parcelize

@Parcelize
data class Address_Item (
    // 결과를 받을 모델
    val uid: String="",
    val userNM: String="",
    val mobileNO: String="",
    val telNO: String="",
    val photo: String="",
    val checkBoxState: Boolean
): Parcelable


Gson 라이브러리를 사용했는데도 불구하고, 데이터가 반환되지 않는 원인을 찾고자 Retrofit2 라이브러리 대신에 Volley 라이브러리를 사용해서 테스트 했다. 두 라이브러리는 동시에 사용할 수가 없더라.

그래서 먼저 Gson 라이브러리 사용없이 for문으로 데이터를 저장하는 걸 시도했다.


package com.link2me.android.recyclerviewjava;

import android.content.Context;
import android.content.DialogInterface;
import android.os.Bundle;
import android.util.Log;
import android.widget.CheckBox;
import android.widget.RelativeLayout;

import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.SearchView;
import androidx.recyclerview.widget.DividerItemDecoration;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;

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.link2me.android.recyclerviewjava.adapter.RecyclerViewAdapter;
import com.link2me.android.recyclerviewjava.model.Address_Item;

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

import java.util.ArrayList;
import java.util.HashMap;

public class MainActivity extends AppCompatActivity {
    private final String TAG = this.getClass().getSimpleName();
    Context context;

    private ArrayList<Address_Item> addressItemList = new ArrayList<>(); // 서버 원본 데이터 리스트
    private ArrayList<Address_Item> searchItemList = new ArrayList<>(); // 검색한 데이터 리스트
    private RecyclerView mRecyclerView;
    private RecyclerViewAdapter mAdapter;

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

        initView();
    }

    private void initView() {
        // Adapter에 추가 데이터를 저장하기 위한 ArrayList
        getServerData(Value.IPADDRESS+"getJSONData.php"); // 서버 데이터 가져오기
    }

    private void getServerData(String SERVER_URL) {
        HashMap<String, String> params = new HashMap<>();
        params.put("keyword", Value.encrypt(Value.URLkey()));
        JSONObject jsonObject = new JSONObject(params);

        // 1. RequestQueue 생성 및 초기화
        RequestQueue requestQueue = Volley.newRequestQueue(context);

        // 2. Request Obejct인 StringRequest 생성
        JsonObjectRequest request = new JsonObjectRequest(Request.Method.POST, SERVER_URL,jsonObject,
                (Response.Listener<JSONObject>) response -> {
                    Log.e("result", "" + response);
                    try {
                        if(response.getString("status").equals("success")){
                            String jsondata = response.getString("addrinfo");
                            showJSONList(jsondata);
                        } else {
                            showAlert(response.getString("status"),response.getString("message"));
                        }
                    } catch (JSONException e) {
                        e.printStackTrace();
                    }
                },
                error -> Log.d("error", "[" + error.getMessage() + "]")) {

        };

        // 3) 생성한 StringRequest를 RequestQueue에 추가
        requestQueue.add(request);
    }

    private void showJSONList(String JSONdata) {
        try {
            JSONArray peoples = new JSONArray(JSONdata); // [] 대괄호
            addressItemList.clear(); // 서버에서 가져온 데이터 초기화
            for(int i=0;i < peoples.length();i++){
                JSONObject jsonObj = peoples.getJSONObject(i);
                final String uid = jsonObj.getString("uid");
                final String name = jsonObj.getString("userNM");
                final String mobileNO = jsonObj.getString("mobileNO");
                final String telNO = jsonObj.getString("telNO");
                final String photo = jsonObj.getString("photo");

                getServerDataList(uid,name,mobileNO,telNO,photo,false);
                selectDataList(uid,name,mobileNO,telNO,photo,false);
            }

            mRecyclerView = findViewById(R.id.address_listview);
            mAdapter = new RecyclerViewAdapter(context,addressItemList);
            LinearLayoutManager manager = new LinearLayoutManager(context);
            DividerItemDecoration decoration = new DividerItemDecoration(context,manager.getOrientation());

            mRecyclerView.addItemDecoration(decoration);
            mRecyclerView.setLayoutManager(manager);
            mRecyclerView.setAdapter(mAdapter);


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

    // 아이템 데이터 추가를 위한 메소드
    public void getServerDataList(String uid, String name, String mobileNO, String officeNO, String photo_image, boolean checkItem_flag) {
        Address_Item item = new Address_Item(uid,name,mobileNO,officeNO,photo_image,checkItem_flag);
        addressItemList.add(item);
    }

    public void selectDataList(String uid, String name, String mobileNO, String officeNO, String photo_image, boolean checkItem_flag) {
        Address_Item item = new Address_Item(uid,name,mobileNO,officeNO,photo_image,checkItem_flag);
        searchItemList.add(item);
    }

    public void showAlert(String title, String message) {
        AlertDialog.Builder builder = new AlertDialog.Builder(context);
        builder.setTitle(title);
        builder.setMessage(message)
                .setCancelable(false)
                .setPositiveButton("OK", new DialogInterface.OnClickListener() {
                    public void onClick(DialogInterface dialog, int id) {
                        dialog.dismiss();
                    }
                });
        AlertDialog alert = builder.create();
        alert.show();
    }

}


다른코드는 첨부파일을 참조하면 된다.

recyclerviewj_1.zip


블로그 이미지

Link2Me

,
728x90

Floating Action Button 은 화면에 떠있는 버튼으로 머터리얼 디자인에서 자주 사용하는 기본 위젯이다.

 

https://www.youtube.com/watch?v=cASXtRa6yDc 동영상을 보면 이해될 것이다.

 

 

앱 build.gradle 추가사항

dependencies {
    implementation 'com.google.android.material:material:1.1.0'
}

 

 

ConstraintLayout 의 모든 보기에는 가로 및 세로 제약조건을 각각 하나 이상 추가해야 한다.

 

 

아래 그림에서 ConstraintLayout 에서 버튼의 위치를 결정하는 것은 1번과 2번이다.

app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"

 

 

 

5번을 누르면 floating 버튼의 색상을 쉽게 변경할 수 있다.

6번은 floating 버튼 안의 이미지를 작성하는 것이다.

Vector Drawable을 사용하고 롤리팝 미만의 기기를 지원해야 할 때는 src 대신에 srcCompat 속성을 사용해야 한다.

 

 

 

 

검색으로 원하는 이미지를 찾아도 된다.

 

이름이 자동으로 추가된 것을 확인할 수 있다.

 

 

 

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello World!"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent"/>

    <com.google.android.material.floatingactionbutton.FloatingActionButton
        android:id="@+id/fabMain"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="16dp"
        android:backgroundTint="#00BCD4"
        android:tooltipText="메시지 전송"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:srcCompat="@drawable/ic_baseline_menu_24"
        tools:ignore="MissingConstraints" />

</androidx.constraintlayout.widget.ConstraintLayout>

 

public class MainActivity extends AppCompatActivity {

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

        FloatingActionButton fab = findViewById(R.id.fabMain);
        fab.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Snackbar.make(view,"실제 클릭 이벤트를 대신 보여준다.",Snackbar.LENGTH_LONG)
                        .setAction("Action",null)
                        .show();
            }
        });
    }
}
 

 

테스트 파일 첨부 (Java 기반 코드인데 코틀린으로 변환 목적으로 앱 build.gradle이 코틀린 관련 추가되어 있음)

floatingbutton.zip
다운로드

 

코틀린으로 코드를 변환해보자.

 

자동변환 전후 Java코드와 kotlin 코드를 보자.

FloatingActionButton fab = findViewById(R.id.fabMain);
fab.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        Snackbar.make(view,"실제 클릭 이벤트를 대신 보여준다.",Snackbar.LENGTH_LONG)
                .setAction("Action",null)
                .show();
    }
});

val fab = findViewById<FloatingActionButton>(R.id.fabMain)
fab.setOnClickListener { view ->
    Snackbar.make(view, "실제 클릭 이벤트를 대신 보여준다.", Snackbar.LENGTH_LONG)
        .setAction("Action", null)
        .show()
}

 

코틀린 코드를 좀 더 정리하면....

fabMain.setOnClickListener { view ->
    Snackbar.make(view, "실제 클릭 이벤트를 대신 보여준다.", Snackbar.LENGTH_LONG)
        .setAction("Action", null)
        .show()
}

 

'안드로이드 > Layout' 카테고리의 다른 글

SMS authentication with OTP Layout Example  (0) 2020.09.14
EditText DatePicker  (0) 2020.08.18
CardView Layout 예제  (0) 2020.04.12
Meterial Design 로그인 Layout 예제  (0) 2020.03.21
ScrollView  (0) 2019.12.20
블로그 이미지

Link2Me

,
728x90

다형성(Polymorphism)

- 같은 타입이지만 실행 결과가 다양한 객체 대입 가능한 성질

- 조상 타입의 참조변수로 자손 타입의 객체를 다룰 수 있는 것이 다형성

 

예제1)

public class Ex7_7 {
    public static void main(String[] args) {
        // 조상타입의 참조변수로 자손타입의 객체를 다룰 수 있는 것이 다형성
        // 반대로 자손타입의 참조변수로 조상타입의 인스턴스를 참조할 수는 없다.
        Car car = null; // 조상 타입의 참조변수
        FireEngine fe = new FireEngine(); // 실제 인스턴스가 무엇인지가 중요
       
        car = fe; // 자손 → 조상으로 형변환. 형변환 생략됨
        car.drive();
       
        FireEngine fe2 = null; // 자손 타입의 참조변수
        fe2 = (FireEngine) car; // 조상 → 자손으로 형변환. 형변환 생략불가.
        fe2.drive();
        fe2.water();
    }
}

class Car {
    String color;
    int door;
   
    void drive() {
        System.out.println("운전중!");
    }
   
    void stop() {
        System.out.println("정지.");
    }
}

class FireEngine extends Car {
    void water() {
        System.out.println("물 뿌리는 중!");
    }
}
 

 

예제1은 문제없이 잘 동작된다.

예제2는 자손 타입의 참조변수로 조상 타입의 인스턴스를 참조할 수 없다는 사항에 위배된다.

 

예제2)

public class Ex7_7 {
    public static void main(String[] args) {
        // 조상타입의 참조변수로 자손타입의 객체를 다룰 수 있는 것이 다형성
        // 반대로 자손타입의 참조변수로 조상타입의 인스턴스를 참조할 수는 없다.
        Car car = new Car(); // 조상 인스턴스
        FireEngine fe = (FireEngine) car; // Car cannot be cast to FireEngine
        fe.drive();
    }
}

class Car {
    String color;
    int door;
   
    void drive() {
        System.out.println("운전중!");
    }
   
    void stop() {
        System.out.println("정지.");
    }
}

class FireEngine extends Car {
    void water() {
        System.out.println("물 뿌리는 중!");
    }
}
 

 

예제3)

public class Polymorphism {
    public static void main(String[] args) {
        // 조상타입의 참조변수로 자손타입의 객체를 다룰 수 있는 것이 다형성
        // 반대로 자손타입의 참조변수로 조상타입의 인스턴스를 참조할 수는 없다.
        Car car = null// 조상 타입의 참조변수
        FireEngine fe = new FireEngine(); // 실제 인스턴스가 무엇인지가 중요
 
        car = fe; // 자손 → 조상으로 형변환. 형변환 생략됨
        car.drive(); // 부모 타입에 선언된 변수와 메소드만 사용 가능
 
        FireEngine fe2 = null// 자손 타입의 참조변수
        if(car instanceof FireEngine){ // 먼저 자식 타입인지 확인 후 강제 타입 실행.
            System.out.println("===== 객체 타입 확인 =====");
            fe2 = (FireEngine) car; // 조상 → 자손으로 형변환. 형변환 생략불가
            fe2.drive();
            fe2.water(); // 자식 타입에 선언된 변수와 메소드를 다시 사용해야 하는 경우
        }
 
        Car car1 = new FireEngine(); // 부모 클래스 변수 = 자식 클래스 타입;
        car1.drive(); // 변환 후에는 부모 클래스 멤버만 접근 가능
 
        System.out.println("===== 상속 처리 =====");
        FireEngine fe3 = new FireEngine();
        fe3.drive();
        fe3.water();
 
        System.out.println("===== 부모 타입 =====");
        Car car2 = new Car();
        // 자식타입의 참조변수로 부모타입의 인스턴스를 참조할 수는 없다.
        FireEngine fe4 = (FireEngine) car2;
        fe4.drive();
 
    }
}
 
class Car {
    String color;
    int door;
 
    void drive() {
        System.out.println("운전중!");
    }
 
    void stop() {
        System.out.println("정지.");
    }
}
 
class FireEngine extends Car {
    void water() {
        System.out.println("물 뿌리는 중!");
    }
}
 

 

 

예제4)

상위클래스 타입의 변수가 복수의 하위클래스 객체를 참조할 수 있도록 하는 것을 다형성이라 한다.

- Product 가 상위 클래스, Tv, Computer, Audio 는 하위 클래스

import java.util.ArrayList;
 
class Product {
    int price;            // 제품의 가격
    int bonusPoint;    // 제품구매 시 제공하는 보너스점수
 
    public Product(int price) {
        this.price = price;
        this.bonusPoint = (int)(price / 10.0);
    }
 
    public Product() {
    }
}
 
class Tv extends Product {
    public Tv() {
        // 조상 클래스의 생성자 Product(int price)를 호출.
        super(100);
    }
 
    @Override
    public String toString(){
        // Object 클래스의 toString()을 오버라이딩한다.
        return "Tv";
    }
}
 
class Computer extends Product {
    public Computer() {
        // 조상 클래스의 생성자 Product(int price)를 호출.
        super(200);
    }
 
    @Override
    public String toString() {
        // Object 클래스의 toString()을 오버라이딩한다.
        return "Computer";
    }
}
 
class Audio extends Product {
    public Audio() {
        // 조상 클래스의 생성자 Product(int price)를 호출.
        super(50);
    }
 
    @Override
    public String toString() {
        // Object 클래스의 toString()을 오버라이딩한다.
        return "Audio";
    }
}
 
class Buyer {
    int money = 1000;      // 소유금액
    int bonusPoint = 0// 보너스점수
    ArrayList<Product> cart = new ArrayList<>();// 구입한 제품을 저장하기 위한 배열
 
    void buy(Product p) { // 매개 변수가 Product 타입의 참조변수
        if(money < p.price) {
            System.out.println("잔액이 부족하여 물건을 살 수 없습니다.");
            return;
        }
 
        money -= p.price;             // 가진 돈에서 구입한 제품의 가격을 뺀다.
        bonusPoint += p.bonusPoint;   // 제품의 보너스 점수를 추가한다.
        cart.add(p);                // 제품을 ArrayList<Product> cart에 추가한다.
        System.out.print(p + "를 구입하셨습니다.");
        System.out.println("현재 잔액은 " + money + " 만원입니다.");
    }
 
    void summary() {                  // 구매한 물품에 대한 정보를 요약해서 보여 준다.
        int sum = 0;                 // 구입한 물품의 가격합계
        String itemList ="";         // 구입한 물품목록
 
        // 반복문을 이용해서 구입한 물품의 총 가격과 목록을 만든다.
        for(int i=0; i < cart.size();i++) {
            sum += cart.get(i).price;
            if(i == cart.size() -1){
                itemList += cart.get(i) + " ";
            } else {
                itemList += cart.get(i) + ", ";
            }
        }
        System.out.println("구입하신 물품의 총 금액은 " + sum + "만원입니다.");
        System.out.println("구입하신 제품은 " + itemList + "입니다.");
    }
}
 
public class Ploymorphism {
    public static void main(String[] args) {
        Buyer b = new Buyer();
 
        b.buy(new Tv());
        b.buy(new Computer());
        b.buy(new Audio());
        b.summary();
    }
}
 

 

 

블로그 이미지

Link2Me

,
728x90

패키지의 선언
-
패키지는 소스파일의 첫 번째 문장으로 단 한번 선언
-
같은 소스 파일의 클래스들은 모두 같은 패키지에 속하게 된다.
-
패키지 선언이 없으면, 이름없는 패키지에 속하게 된다.

- 패키지명은 선언시 반드시 소문자로 선언해야 한다.


import
-
클래스를 사용할 때 패키지 이름을 생략할 수 있다.
-
컴파일러에게 클래스가 속한 패키지를 알려준다.
-
java.lang 패키지의 클래스는 import 하지 않고도 사용할 수 있다.
- import
문은 패키지문과 클래스 선언의 사이에 선언한다.

접근 제어자

제어자

같은 클래스

같은 패키지

자손 클래스

전체

 public





 protected



 

 (default)

 

 

private

 

 

 


아래 코드는 Eclipse 툴이 아니라 Android Studio 에서 Android 어플 작성을 위한 코드 생성으로 테스트했다.

개념은 동일하므로 Eclipse, Android Studio 상관 없다.

package com.link2me.android.pkg1;

public class MyParent { // 접근 제어자가 public
    private int prv; // 같은 클래스
    int dft; // 같은 패키지
    protected int prt; // 같은/다른 패키지 + 자손
    public int pub; // 접근 제한 없음

    public void printMembers() {
        System.out.println(prv); // OK, 같은 클래스
        System.out.println(dft); // OK, 같은 클래스
        System.out.println(prt); // OK, 같은 클래스
        System.out.println(pub); // OK, 같은 클래스
    }
}

class MyParentTest { // 접근제어자가 default
    // 하나의 파일에 public class 이름이 2개 오면 에러가 발생한다.
    // 같은 패키지
    public void printMembers(){
        MyParent mp = new MyParent();
        //System.out.println(mp.prv); // 에러, private은 같은 class 내에서만 접근 가능
        System.out.println(mp.dft); // OK, default는 같은 패키지내에서 접근 가능
        System.out.println(mp.prt); // OK, protected는 같은/다른 패키지 + 자손
        System.out.println(mp.pub); // OK, public은 접근 제한이 전혀 없다
    }

package com.link2me.android.pkg2;

import com.link2me.android.pkg1.MyParent;

public class MyChild extends MyParent { // 상속 받음
    // 다른 패키지 + 자손
    public void printMembers(){
        //System.out.println(prv); // 에러, private은 같은 class 내에서만 접근 가능
        //System.out.println(dft); // 에러, default는 같은 패키지내에서만 접근 가능
        System.out.println(prt); // OK, protected는 같은/다른 패키지 + 자손
        System.out.println(pub); // OK, public은 접근 제한이 전혀 없다
    }
}

class MyParentTest { // 접근제어자가 default
    // 하나의 파일에 public class 이름이 2개 오면 에러가 발생한다.
    // 다른 패키지 + 다른 class
    public void printMembers(){
        MyParent mp = new MyParent();
        //System.out.println(mp.prv); // 에러 private은 같은 class 내에서만 접근 가능
        //System.out.println(mp.dft); // 에러, default는 같은 패키지내에서만 접근 가능
        //System.out.println(mp.prt); // 에러, protected는 같은/다른 패키지 + 자손
        System.out.println(mp.pub); // OK, public은 접근 제한이 전혀 없다
    }
}


블로그 이미지

Link2Me

,
728x90

개발툴 설치를 기본으로 설치하지 않고 내가 원하는 디렉토리를 지정하여 설치할 수 있다.



이렇게 필요한 것을 설치하면 된다.

블로그 이미지

Link2Me

,
728x90

Last Updated 2020.6.26


테스트 환경 : 삼성 갤럭시노트9 (AndroidQ)


안드로이드폰의 운영체제를 업그레이드하여 8.0 이다. (9.0 이상 사용폰은 마지막에 수정된 부분만 별도 언급)

API 23까지는 APK 파일을 서버에서 다운로드받고 자동으로 설치하는 화면을 띄우는 코드는

Intent intent = new Intent(Intent.ACTION_VIEW);
 Uri apkUri = Uri.fromFile(outputFile);
 intent.setDataAndType(apkUri, "application/vnd.android.package-archive");
 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
 context.startActivity(intent);


Android 7.0(Nougat) 이상에서는 위 코드로 실행하면 에러가 발생한다.

앱 외부에서 file:// URI의 노출을 금지하는 StrictMode API 정책을 적용한다.
파일 URI를 포함하는 인텐트가 앱을 떠나면 FileUriExposedException 예외와 함께 앱에 오류가 발생한다.


애플리케이션 간에 파일을 공유하려면 content:// URI를 보내고 이 URI에 대해 임시 액세스 권한을 부여해야 한다.
FileProvider 그 권한을 가장 쉽게 부여하는 방법이다.


구글링해서 찾은 대부분의 자료들이 개념 중심으로 적혀 있어 내용 파악이 쉽지 않더라. 테스트한 코드 핵심 내용을 모두 적었다.


provider_paths.xml

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


<files-path name="files" path="." />


File path = getFilesDir();
File file = new File(path, "abc.apk");


파일 경로를 변경해서 테스트 해보니 마찬가지로 잘 동작된다.

https://stackoverflow.com/questions/37074872/android-fileprovider-on-custom-external-storage-folder 참조


<external-path name="download" path="." />

File path = new File(Environment.getExternalStorageDirectory() + "/download");



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.tistory.link2me.filedownload">

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

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

    <application
        android:allowBackup="false"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme.NoActionBar"

        android:usesCleartextTraffic="true">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>


        <provider
            android:name="androidx.core.content.FileProvider"
            android:authorities="${applicationId}.fileprovider"
            android:exported="false"
            android:grantUriPermissions="true">
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/provider_paths"
                tools:replace="android:resource" />
        </provider>

        <activity android:name=".DownloadAPK" />
    </application>

</manifest>



MainActivity.java

import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;

import com.gun0912.tedpermission.PermissionListener;
import com.gun0912.tedpermission.TedPermission;

import java.util.ArrayList;

public class MainActivity extends AppCompatActivity {
    Context context;

    PermissionListener permissionlistener = new PermissionListener() {
        @Override
        public void onPermissionGranted() {
            MainActivity.this.Button_Click();
        }

        @Override
        public void onPermissionDenied(ArrayList<String> deniedPermissions) {
            Toast.makeText(MainActivity.this, "권한 허용을 하지 않으면 서비스를 이용할 수 없습니다.", Toast.LENGTH_SHORT).show();
        }
    };

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

        if (Build.VERSION.SDK_INT >= 23) {
            TedPermission.with(this)
                    .setPermissionListener(permissionlistener)
                    .setRationaleMessage("파일을 다운로드 하기 위해서는 접근 권한이 필요합니다")
                    .setDeniedMessage("앱에서 요구하는 권한설정이 필요합니다...\n [설정] > [권한] 에서 사용으로 활성화해주세요.")
                    .setPermissions(new String[]{"android.permission.WRITE_EXTERNAL_STORAGE", "android.permission.READ_EXTERNAL_STORAGE"})
                    .check();
        } else {
            Button_Click();
        }

    }

    private void Button_Click() {
        Button btn_dnload = (Button) findViewById(R.id.btn_image_download);
        btn_dnload.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Intent intent = new Intent(MainActivity.this, DownloadAPK.class);
                startActivity(intent);
            }
        });
    }
}


Value.java

public class Value extends Activity {
    public static final String APKNAME = "ABC.apk"; // APK name
    public static final String IPADDRESS = "http://100.100.100.100";
}



DownloadAPK.java

import android.content.Context;
import android.content.Intent;
import android.media.MediaScannerConnection;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.support.v4.content.FileProvider;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URL;
import java.net.URLConnection;

public class DownloadAPK extends AppCompatActivity {
    Context context;
    private File outputFile;
    ProgressBar progressBar;
    TextView textView;
    LinearLayout linearLayout;
    DownloadFileFromURL downloadFileAsyncTask;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.downloadapk_activity);
        context = this.getBaseContext();

        linearLayout = (LinearLayout) findViewById(R.id.downloadprogress_layout);
        textView = (TextView) findViewById(R.id.txtView01);
        progressBar = (ProgressBar) findViewById(R.id.progressBar);

        DownloadAPK();
    }

    private void DownloadAPK() {
        // 백그라운드 객체를 만들어 주어야 다운로드 취소가 제대로 동작됨
        downloadFileAsyncTask = new DownloadFileFromURL();
        downloadFileAsyncTask.execute(Value.IPADDRESS + "/Download.php");
    }

    class DownloadFileFromURL extends AsyncTask<String, Integer, String> {

        @Override
        protected void onPreExecute() {
            super.onPreExecute();
            progressBar.setProgress(0);
        }

        @Override
        protected String doInBackground(String... apkurl) {
            int count;
            int lenghtOfFile = 0;
            InputStream input = null;
            OutputStream fos = null;

            try {
                URL url = new URL(apkurl[0]);
                URLConnection connection = url.openConnection();
                connection.connect();

                lenghtOfFile = connection.getContentLength(); // 파일 크기를 가져옴

                File path = getFilesDir();
                outputFile = new File(path, Value.APKNAME);
                if (outputFile.exists()) { // 기존 파일 존재시 삭제하고 다운로드
                    outputFile.delete();
                }

                input = new BufferedInputStream(url.openStream());
                fos = new FileOutputStream(outputFile);
                byte data[] = new byte[1024];
                long total = 0;

                while ((count = input.read(data)) != -1) {
                    if (isCancelled()) {
                        input.close();
                        return String.valueOf(-1);
                    }
                    total = total + count;
                    if (lenghtOfFile > 0) { // 파일 총 크기가 0 보다 크면
                        publishProgress((int) (total * 100 / lenghtOfFile));
                    }
                    fos.write(data, 0, count); // 파일에 데이터를 기록
                }

                fos.flush();

            } catch (Exception e) {
                e.printStackTrace();
                Log.e("UpdateAPP", "Update error! " + e.getMessage());
            } finally {
                if (input != null) {
                    try {
                        input.close();
                    }
                    catch(IOException ioex) {
                    }
                }
                if (fos != null) {
                    try {
                        fos.close();
                    }
                    catch(IOException ioex) {
                    }
                }
            }
            return null;
        }

        protected void onProgressUpdate(Integer... progress) {
            super.onProgressUpdate(progress);
            // 백그라운드 작업의 진행상태를 표시하기 위해서 호출하는 메소드
            progressBar.setProgress(progress[0]);
            textView.setText("다운로드 : " + progress[0] + "%");
        }

        protected void onPostExecute(String result) {
            if (result == null) {
                progressBar.setProgress(0);
                Toast.makeText(getApplicationContext(), "다운로드 완료되었습니다.", Toast.LENGTH_LONG).show();

                System.out.println("getPackageName : "+getPackageName());
                System.out.println("APPLICATION_ID Path : "+BuildConfig.APPLICATION_ID);
                System.out.println("outputFile Path : "+ outputFile.getAbsolutePath());
                System.out.println("Fie getPath : "+ outputFile.getPath());

                // 미디어 스캐닝
                MediaScannerConnection.scanFile(getApplicationContext(), new String[]{outputFile.getAbsolutePath()}, null, new MediaScannerConnection.OnScanCompletedListener() {
                    @Override
                    public void onScanCompleted(String s, Uri uri) {

                    }
                });


                // 다운로드한 파일 실행하여 업그레이드 진행하는 코드
                if (Build.VERSION.SDK_INT >= 24) {
                    // Android Nougat ( 7.0 ) and later
                    installApk(outputFile);
                    System.out.println("SDK_INT 24 이상 ");
                } else {
                    Intent intent = new Intent(Intent.ACTION_VIEW);
                    Uri apkUri = Uri.fromFile(outputFile);
                    intent.setDataAndType(apkUri, "application/vnd.android.package-archive");
                    intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                    getApplicationContext().startActivity(intent);
                    System.out.println("SDK_INT 23 이하 ");
                }

            } else {
                Toast.makeText(getApplicationContext(), "다운로드 에러", Toast.LENGTH_LONG).show();
            }
        }

        protected void onCancelled() {
            // cancel메소드를 호출하면 자동으로 호출되는 메소드
            progressBar.setProgress(0);
            textView.setText("다운로드 진행 취소됨");
        }
    }

    public void installApk(File file) {
        Uri uri = FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".fileprovider",file);
        Intent intent = new Intent(Intent.ACTION_VIEW);
        intent.setDataAndType(uri, "application/vnd.android.package-archive");
        intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
        context.startActivity(intent);
    }

}

위 분홍색 installApk() 메서드는 에러가 발생할 것이다.

BuildConfig.APPLICATION_ID 를 제대로 인식하지 못하는 문제더라.


    public void installApk(File file) {
        Uri fileUri = FileProvider.getUriForFile(context, context.getApplicationContext().getPackageName() + ".fileprovider",file);
        Intent intent = new Intent(Intent.ACTION_VIEW);
        intent.setDataAndType(fileUri, "application/vnd.android.package-archive");
        intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK);
        intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
        startActivity(intent);
        finish();
    } 

이 코드로 대체하면 정상적으로 앱 업데이트가 이루어진다.


앱 build.gradle

apply plugin: 'com.android.application'
// 코틀린 혼용 사용을 위해 추가
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'

android {
    compileSdkVersion 29
    buildToolsVersion "29.0.3"

    defaultConfig {
        applicationId "com.link2me.android.abc"
        minSdkVersion 23
        targetSdkVersion 29
        versionCode 1
        versionName "1.0"
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    compileOptions {
        sourceCompatibility = 1.8
        targetCompatibility = 1.8
    }

}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" // 코틀린 혼용 사용을 위해 추가
    implementation 'androidx.appcompat:appcompat:1.1.0'
    implementation 'androidx.core:core-ktx:1.2.0' // 코틀린 혼용 사용을 위해 추가
    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'

    implementation 'com.journeyapps:zxing-android-embedded:3.6.0'
    implementation 'gun0912.ted:tedpermission:2.0.0'
    implementation 'com.google.android.material:material:1.0.0'

    implementation 'com.naver.maps:map-sdk:3.7.1' // 네이버 지도 SDK
    implementation 'com.google.android.gms:play-services-location:17.0.0'
}


안드로이드 8.0 출처를 알 수 없는 앱 설정화면 코드는 http://mixup.tistory.com/78 참조해서 해결했다.


Value.APKNAME 은 "abc.apk" 와 같은 명칭이다.
미디어 스캐닝은 파일 다운로드한 폴더의 내용을 확인할 목적으로 검색하다 알게된 걸 적용해봤다.


참고 사이트

https://pupli.net/2017/06/18/install-apk-files-programmatically/


http://stickyny.tistory.com/110


http://mixup.tistory.com/98


도움이 되셨다면 광고 클릭 해 주세요. 좋은 글 작성에 큰 힘이 됩니다

출처: https://link2me.tistory.com/1110 [소소한 일상 및 업무TIP 다루기]

도움이 되셨다면 댓글 달아주세요. 좋은 글 작성에 큰 힘이 됩니다.

블로그 이미지

Link2Me

,
728x90

Java 코드로 만든 코드를 코틀린으로 변환하고 테스트를 하니까 제대로 동작이 안된다.

코드 일부를 수정해주고 나서 동작에 제대로 된다.

Volley 라이브리를 사용하면 간단하게 해결될 사항이지만 HttpURLConnection 으로 서버 데이터를 안드로이드폰으로 가져오는 방법도 알아두면 좋을 거 같아서 테스트하고 적어둔다.


private fun UpgradeChk() {
     val builder = Uri.Builder()
         .appendQueryParameter("os", "a")
     val postParams = builder.build().encodedQuery
     val getlastVersion = getlastVersion()
     getlastVersion.execute( Value.IPADDRESS + "/lastVersion.php", postParams)
 }

inner class getlastVersion : AsyncTask<String, Void, String>() {
    override fun doInBackground(vararg params: String): String {
        return try {
            HttpURLComm.getJson(params[0], params[1])
        } catch (e: Exception) {
            String.format("Exception: " + e.message)
        }
    }

    override fun onPostExecute(response: String) {
        version = Value.VERSION // 앱 버전
        version = version.replace("[^0-9]".toRegex(), "") // 버전에서 숫자만 추출
        Log.e("WEB", "Response: $response")
        val Response = response.replace("[^0-9]".toRegex(), "") // 버전에서 숫자만 추출
        println("Server Version : $Response")
        if (version.toInt() < Response.toInt()) { // 서버 버전이 더 높으면
            UpgradeProcess()
        } else {
            AutoLoginProgress()
        }
    }
}
 

import android.util.Log
import android.webkit.CookieManager
import com.link2me.android.enode.Value
import java.io.BufferedReader
import java.io.DataOutputStream
import java.net.HttpURLConnection
import java.net.URL

object HttpURLComm  {
    // serverURL : JSON 요청을 받는 서버의 URL
    // postParams : POST 방식으로 전달될 입력 데이터
    // 반환 데이터 : 서버에서 전달된 JSON 데이터
    @JvmStatic
    @Throws(Exception::class)
    fun getJson(serverUrl: String?, postParams: String?): String {
        try {
            Thread.sleep(100)
            val url = URL(serverUrl)
            val conn = url.openConnection() as HttpURLConnection
            // 세션 쿠키 전달
            val cookieString = CookieManager.getInstance().getCookie(Value.IPADDRESS)
            val sb = StringBuilder()
            if (conn != null) { // 연결되었으면
                //add request header
                conn.requestMethod = "POST"
                conn.setRequestProperty("USER-AGENT", "Mozilla/5.0")
                conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded")
                conn.setRequestProperty("Accept-Language", "en-US,en;q=0.5")
                if (cookieString != null) {
                    conn.setRequestProperty("Cookie", cookieString)
                    Log.e("PHP_getCookie", cookieString)
                }
                conn.connectTimeout = 10000
                conn.readTimeout = 10000
                conn.useCaches = false
                conn.defaultUseCaches = false
                conn.doOutput = true // POST 로 데이터를 넘겨주겠다는 옵션
                conn.doInput = true

                // Send post request
                val wr = DataOutputStream(conn.outputStream)
                wr.writeBytes(postParams)
                wr.flush()
                wr.close()
                val responseCode = conn.responseCode
                Log.e("TAG","GET Response Code : $responseCode")
                if (responseCode == HttpURLConnection.HTTP_OK) { // 연결 코드가 리턴되면
                    val allText: String = conn.inputStream.bufferedReader().use(BufferedReader::readText)
                    sb.append(allText.trim())
                }
            }
            conn.disconnect()
            return sb.toString()
            // 수행이 끝나고 리턴하는 값은 다음에 수행될 onProgressUpdate 의 파라미터가 된다
        } catch (e: Exception) {
            e.printStackTrace()
        } finally {

        }
        return ""
    }
}


https://github.com/irontec/android-kotlin-samples/blob/master/KotlinTest/app/src/main/java/com/irontec/examples/kotlintest/HttpUrlConnectionAsyncActivity.kt

를 참조하여 코드를 약간 보완했다.

object HttpURLComm {
    // serverURL : JSON 요청을 받는 서버의 URL
    // postParams : POST 방식으로 전달될 입력 데이터
    // 반환 데이터 : 서버에서 전달된 JSON 데이터
    @JvmStatic
    @Throws(Exception::class)
    fun getJson(serverUrl: String?, postParams: String?): String {
        Thread.sleep(100)
        val url = URL(serverUrl)
        val httpClient = url.openConnection() as HttpURLConnection
        // 세션 쿠키 전달
        val cookieString = CookieManager.getInstance().getCookie(Value.IPADDRESS)
        val sb = StringBuilder()
        if (httpClient != null) { // 연결되었으면
            //add request header
            httpClient.requestMethod = "POST"
            httpClient.setRequestProperty("USER-AGENT", "Mozilla/5.0")
            httpClient.setRequestProperty("Content-Type", "application/x-www-form-urlencoded")
            httpClient.setRequestProperty("Accept-Language", "en-US,en;q=0.5")
            if (cookieString != null) {
                httpClient.setRequestProperty("Cookie", cookieString)
                Log.e("PHP_getCookie", cookieString)
            }
            httpClient.connectTimeout = 10000
            httpClient.readTimeout = 10000
            httpClient.useCaches = false
            httpClient.defaultUseCaches = false
            httpClient.doOutput = true // POST 로 데이터를 넘겨주겠다는 옵션
            httpClient.doInput = true

            // Send post request
            val wr = DataOutputStream(httpClient.outputStream)
            wr.writeBytes(postParams)
            wr.flush()
            wr.close()

            if (httpClient.responseCode == HttpURLConnection.HTTP_OK) {
                try {
                    val stream = BufferedInputStream(httpClient.inputStream)
                    val data: String = readStream(inputStream = stream)
                    return data
                } catch (e: Exception) {
                    e.printStackTrace()
                } finally {
                    httpClient.disconnect()
                }
            } else {
                println("ERROR ${httpClient.responseCode}")
            }
        }
        return ""
    }

    fun readStream(inputStream: BufferedInputStream): String {
        val bufferedReader = BufferedReader(InputStreamReader(inputStream))
        val stringBuilder = StringBuilder()
        bufferedReader.forEachLine { stringBuilder.append(it) }
        return stringBuilder.toString()
    }
}
 



블로그 이미지

Link2Me

,
728x90

https://link2me.tistory.com/1703 게시글에 작성된 것을 코틀린으로 변경한 것이다.


dependencies {
    implementation fileTree(dir: "libs", include: ["*.jar"])
    implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
    implementation 'androidx.core:core-ktx:1.3.0'
    implementation 'androidx.appcompat:appcompat:1.1.0'
    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'

    implementation 'com.google.android.material:material:1.1.0'
    implementation 'gun0912.ted:tedpermission:2.0.0'
    implementation 'com.google.android.gms:play-services-maps:17.0.0' // 구글지도 라이브러리
    implementation 'com.google.android.gms:play-services-location:17.0.0' // 위치정보 라이브러리
}

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


activity_current_place.xml

<?xml version="1.0" encoding="utf-8"?>
<fragment xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:map="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/realtimemap"
    android:name="com.google.android.gms.maps.SupportMapFragment"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MapsActivity" />


Android Studio 에서 제공하는 Java파일을 코틀린 파일로 변환하는 기능을 이용해서 변환하고 일부 코드를 좀 수정했다.

package com.link2me.android.googlemap

import android.Manifest
import android.annotation.SuppressLint
import android.content.Context
import android.content.DialogInterface
import android.content.pm.PackageManager
import android.location.Address
import android.location.Geocoder
import android.location.Location
import android.os.Bundle
import android.os.Looper
import android.telephony.SmsManager
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.widget.EditText
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import com.google.android.gms.location.*
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.*
import java.io.IOException
import java.text.SimpleDateFormat
import java.util.*

class CurrentPlace : AppCompatActivity(), OnMapReadyCallback {
    private val TAG = this.javaClass.simpleName
    private lateinit var mContext: Context
    private lateinit var mMap: GoogleMap
    private var currentMarker: Marker? = null

    private lateinit var mFusedLocationProviderClient : FusedLocationProviderClient
    private lateinit var locationRequest: LocationRequest
    private var mCurrentLocatiion: Location? = null
    private var mCameraPosition: CameraPosition? = null
    private val mDefaultLocation = LatLng(37.56, 126.97)
    private var mLocationPermissionGranted = false
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        savedInstanceState?.let{
            mCurrentLocatiion = it.getParcelable(KEY_LOCATION)
            mCameraPosition = it.getParcelable(KEY_CAMERA_POSITION)
        }
        setContentView(R.layout.activity_current_place)
        mContext = this@CurrentPlace

        locationRequest = LocationRequest()
            .setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY) // 정확도를 최우선적으로 고려
            .setInterval(UPDATE_INTERVAL_MS.toLong()) // 위치가 Update 되는 주기
            .setFastestInterval(FASTEST_UPDATE_INTERVAL_MS.toLong()) // 위치 획득후 업데이트되는 주기
        val builder = LocationSettingsRequest.Builder()
        builder.addLocationRequest(locationRequest)

        // Construct a FusedLocationProviderClient.
        mFusedLocationProviderClient = LocationServices.getFusedLocationProviderClient(this)

        // Build the map.
        val mapFragment = supportFragmentManager.findFragmentById(R.id.realtimemap) as SupportMapFragment
        mapFragment.getMapAsync(this)
    }

    /**
     * Saves the state of the map when the activity is paused.
     */
    override fun onSaveInstanceState(outState: Bundle) {
        mMap?.let{
            outState.putParcelable(KEY_CAMERA_POSITION, it.cameraPosition)
            outState.putParcelable(KEY_LOCATION, mCurrentLocatiion)
            super.onSaveInstanceState(outState)
        }
    }

    override fun onMapReady(map: GoogleMap) {
        Log.e(TAG, "onMapReady :")
        mMap = map
        setDefaultLocation() // GPS를 찾지 못하는 장소에 있을 경우 지도의 초기 위치가 필요함.
        locationPermission
        updateLocationUI()
        deviceLocation
    }

    private fun updateLocationUI() {
        mMap?.let{
            try {
                if (mLocationPermissionGranted) {
                    it.isMyLocationEnabled = true
                    it.uiSettings.isMyLocationButtonEnabled = true
                } else {
                    it.isMyLocationEnabled = false
                    it.uiSettings.isMyLocationButtonEnabled = false
                    mCurrentLocatiion = null
                    locationPermission
                }
            } catch (e: SecurityException) {
                Log.e("Exception: %s", e.message!!)
            }
        }
    }

    private fun setDefaultLocation() {
        currentMarker?.let{
            it.remove()
        }
        val markerOptions = MarkerOptions()
        markerOptions.position(mDefaultLocation)
        markerOptions.title("위치정보 가져올 수 없음")
        markerOptions.snippet("위치 퍼미션과 GPS 활성 여부 확인하세요")
        markerOptions.draggable(true)
        markerOptions.icon(BitmapDescriptorFactory.defaultMarker(BitmapDescriptorFactory.HUE_RED))
        currentMarker = mMap!!.addMarker(markerOptions)
        val cameraUpdate = CameraUpdateFactory.newLatLngZoom(mDefaultLocation, 15f)
        mMap.moveCamera(cameraUpdate)
    }

    fun getCurrentAddress(latlng: LatLng): String {
        // 위치 정보와 지역으로부터 주소 문자열을 구한다.
        var addressList: List<Address>? = null
        val geocoder =
            Geocoder(this, Locale.getDefault())

        // 지오코더를 이용하여 주소 리스트를 구한다.
        addressList = try {
            geocoder.getFromLocation(latlng.latitude, latlng.longitude, 1)
        } catch (e: IOException) {
            Toast.makeText(
                this,
                "위치로부터 주소를 인식할 수 없습니다. 네트워크가 연결되어 있는지 확인해 주세요.",
                Toast.LENGTH_SHORT
            ).show()
            e.printStackTrace()
            return "주소 인식 불가"
        }
        if (addressList.size < 1) { // 주소 리스트가 비어있는지 비어 있으면
            return "해당 위치에 주소 없음"
        }

        // 주소를 담는 문자열을 생성하고 리턴
        val address = addressList[0]
        val addressStringBuilder = StringBuilder()
        for (i in 0..address.maxAddressLineIndex) {
            addressStringBuilder.append(address.getAddressLine(i))
            if (i < address.maxAddressLineIndex) addressStringBuilder.append("\n")
        }
        return addressStringBuilder.toString()
    }

    var locationCallback: LocationCallback = object : LocationCallback() {
        override fun onLocationResult(locationResult: LocationResult) {
            super.onLocationResult(locationResult)
            val locationList =
                locationResult.locations
            if (locationList.size > 0) {
                val location = locationList[locationList.size - 1]
                val currentPosition =
                    LatLng(location.latitude, location.longitude)
                val markerTitle = getCurrentAddress(currentPosition)
                val markerSnippet =
                    "위도:" + location.latitude.toString() + " 경도:" + location.longitude
                        .toString()
                Log.d(TAG,"Time :" + CurrentTime() + " onLocationResult : " + markerSnippet)

                //현재 위치에 마커 생성하고 이동
                setCurrentLocation(location, markerTitle, markerSnippet)
                mCurrentLocatiion = location
            }
        }
    }

    private fun CurrentTime(): String {
        val today = Date()
        val date = SimpleDateFormat("yyyy/MM/dd")
        val time = SimpleDateFormat("hh:mm:ss a")
        return time.format(today)
    }

    fun setCurrentLocation(
        location: Location,
        markerTitle: String?,
        markerSnippet: String?
    ) {
        if (currentMarker != null) currentMarker!!.remove()
        val currentLatLng = LatLng(location.latitude, location.longitude)
        val markerOptions = MarkerOptions()
        markerOptions.position(currentLatLng)
        markerOptions.title(markerTitle)
        markerOptions.snippet(markerSnippet)
        markerOptions.draggable(true)
        currentMarker = mMap!!.addMarker(markerOptions)
        mMap.setOnMarkerClickListener {
            val inflater =
                mContext!!.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
            val layout = inflater.inflate(
                R.layout.send_message_popup,
                null
            )
            val lms_confirm =
                AlertDialog.Builder(mContext!!)
            lms_confirm.setTitle("발송할 휴대폰번호 등록")
            lms_confirm.setView(layout)
            val etphoneNO =
                layout.findViewById<View>(R.id.phoneno) as EditText
            // 확인 버튼 설정
            lms_confirm.setPositiveButton(
                "등록",
                DialogInterface.OnClickListener { dialog, which ->
                    val phoneNO =
                        etphoneNO.text.toString().trim { it <= ' ' }
                    if (phoneNO.length == 0) {
                        val phoneNO_confirm =
                            AlertDialog.Builder(mContext!!)
                        phoneNO_confirm.setMessage("휴대폰 번호를 입력하세요.").setCancelable(false)
                            .setPositiveButton(
                                "확인"
                            ) { dialog, which -> // 'YES'
                                dialog.dismiss()
                            }
                        val alert =
                            phoneNO_confirm.create()
                        alert.show()
                        return@OnClickListener
                    }
                    val gps_location =
                        location.latitude.toString() + "," + location.longitude
                            .toString()
                    SMS_Send(phoneNO, gps_location)
                })
            lms_confirm.setNegativeButton(
                "취소"
            ) { dialog, which -> dialog.dismiss() }
            lms_confirm.show()
            true
        }
        val cameraUpdate = CameraUpdateFactory.newLatLng(currentLatLng)
        mMap.moveCamera(cameraUpdate)
    }

    private fun SMS_Send(phoneNO: String, message: String ) { // SmsManager API
        var sms_message = "구글 지도 위치를 보내왔습니다.\n"
        sms_message += "http://maps.google.com/maps?f=q&q=$message\n누르면 상대방의 위치를 확인할 수 있습니다."
        Log.e(TAG, "SMS : $sms_message")
        try {
            //전송
            val smsManager = SmsManager.getDefault()
            val parts = smsManager.divideMessage(sms_message)
            smsManager.sendMultipartTextMessage(phoneNO, null, parts, null, null)
            Toast.makeText(
                applicationContext,
                "위치전송 문자보내기 완료!",
                Toast.LENGTH_LONG
            ).show()
        } catch (e: Exception) {
            Toast.makeText(
                applicationContext,
                "SMS faild, please try again later!",
                Toast.LENGTH_LONG
            ).show()
            e.printStackTrace()
        }
    }

    private val deviceLocation: Unit
        private get() {
            try {
                if (mLocationPermissionGranted) {
                    mFusedLocationProviderClient!!.requestLocationUpdates(
                        locationRequest,
                        locationCallback,
                        Looper.myLooper()
                    )
                }
            } catch (e: SecurityException) {
                Log.e("Exception: %s", e.message!!)
            }
        }

    private val locationPermission: Unit
        private get() {
            if (ContextCompat.checkSelfPermission(this.applicationContext,
                    Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED
            ) {
                mLocationPermissionGranted = true
            } else {
                ActivityCompat.requestPermissions(
                    this, arrayOf(Manifest.permission.ACCESS_FINE_LOCATION),
                    PERMISSIONS_REQUEST_ACCESS_FINE_LOCATION
                )
            }
        }

    override fun onRequestPermissionsResult(
        requestCode: Int,
        permissions: Array<String>,
        grantResults: IntArray
    ) {
        mLocationPermissionGranted = false
        when (requestCode) {
            PERMISSIONS_REQUEST_ACCESS_FINE_LOCATION -> {
                if (grantResults.size > 0
                    && grantResults[0] == PackageManager.PERMISSION_GRANTED
                ) {
                    mLocationPermissionGranted = true
                }
            }
        }
        updateLocationUI()
    }

    @SuppressLint("MissingPermission")
    override fun onStart() {
        super.onStart()
        if (mLocationPermissionGranted) {
            Log.d(TAG, "onStart : requestLocationUpdates")
            mFusedLocationProviderClient!!.requestLocationUpdates(
                locationRequest,
                locationCallback,
                null
            )
            mMap?.let{
                it.isMyLocationEnabled = true
            }
        }
    }

    override fun onStop() {
        super.onStop()
        mFusedLocationProviderClient?.let{
            it!!.removeLocationUpdates(locationCallback)
            Log.d(TAG, "onStop : removeLocationUpdates")
        }
    }

    override fun onDestroy() {
        super.onDestroy()
        mFusedLocationProviderClient?.let{
            it.removeLocationUpdates(locationCallback)
            Log.d(TAG, "onDestroy : removeLocationUpdates")
        }
    }

    companion object {
        private const val DEFAULT_ZOOM = 15
        private const val PERMISSIONS_REQUEST_ACCESS_FINE_LOCATION = 1
        private const val GPS_ENABLE_REQUEST_CODE = 2001
        private const val UPDATE_INTERVAL_MS = 1000 * 60 * 15 // 1분 단위 시간 갱신
        private const val FASTEST_UPDATE_INTERVAL_MS = 1000 * 30 // 30초 단위로 화면 갱신
        private const val KEY_CAMERA_POSITION = "camera_position"
        private const val KEY_LOCATION = "location"
    }
}




블로그 이미지

Link2Me

,
728x90

위험권한을 편리하게 체크해주는 tedpermission 라이브러리를 코틀린으로 변환한 것과 뒤로 가기를 두번 연속 누르면 종료되는 함수를 적어둔다.

구글지도를 간단하게 구현할 목적이라 dependencies에 구글 라이브러리가 추가되어 있다.

 

앱 build.gradle

apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'

android {
    compileSdkVersion 30
    buildToolsVersion "30.0.0"

    defaultConfig {
        applicationId "com.link2me.android.googlemap"
        minSdkVersion 21
        targetSdkVersion 30
        versionCode 1
        versionName "1.0"
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    compileOptions {
        targetCompatibility JavaVersion.VERSION_1_8
        sourceCompatibility JavaVersion.VERSION_1_8
    }

}

dependencies {
    implementation fileTree(dir: "libs", include: ["*.jar"])
    implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
    implementation 'androidx.core:core-ktx:1.3.0'
    implementation 'androidx.appcompat:appcompat:1.1.0'
    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'

    implementation 'com.google.android.gms:play-services-maps:17.0.0'
    implementation 'com.google.android.gms:play-services-location:17.0.0'
    implementation 'gun0912.ted:tedpermission:2.0.0'
    implementation 'com.android.volley:volley:1.1.1'
    implementation 'com.google.android.material:material:1.1.0'

}

2021.7월 확인 사항

implementation 'io.github.ParkSangGwon:tedpermission:2.3.0' 로 변경되었더라.

 

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.link2me.android.googlemap">

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

    <!-- 도시 블록 내에서 정확한 위치(네트워크 위치) -->
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

    <!-- 정확한 위치 확보(네트워크 위치 + GPS 위치) -->

    <application
        android:allowBackup="false"
        android:icon="@drawable/icon"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme"
        android:usesCleartextTraffic="true">
        <activity android:name=".MapsActivity"></activity>
        <activity
            android:name=".SplashActivity"
            android:theme="@style/AppTheme.NoActionBar">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

        <meta-data
            android:name="com.google.android.geo.API_KEY"
            android:value="@string/google_maps_key" />

        <provider
            android:name="androidx.core.content.FileProvider"
            android:authorities="${applicationId}.fileprovider"
            android:exported="false"
            android:grantUriPermissions="true">
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/provider_paths"
                tools:replace="android:resource" />
        </provider>
    </application>

</manifest>

 

SplashActivity.kt

import android.Manifest
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.provider.Settings
import android.util.Log
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import com.gun0912.tedpermission.PermissionListener
import com.gun0912.tedpermission.TedPermission

class SplashActivity : AppCompatActivity() {
    var mContext: Context? = null
    private val SPLASH_TIME_OUT: Long = 2000 // 2 sec

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        mContext = this@SplashActivity;
        checkPermissions()
    }

    var permissionlistener: PermissionListener = object : PermissionListener {
        override fun onPermissionGranted() {
            initView()
        }

        override fun onPermissionDenied(deniedPermissions: MutableList<String?>?) {
            Toast.makeText(
                this@SplashActivity,
                "권한 허용을 하지 않으면 서비스를 이용할 수 없습니다.",
                Toast.LENGTH_SHORT
            ).show()
        }
    }

    private fun checkPermissions() {
        if (Build.VERSION.SDK_INT >= 26) { // 출처를 알 수 없는 앱 설정 화면 띄우기
            val pm: PackageManager = mContext!!.getPackageManager()
            Log.e("PackageName", packageName)
            if (!pm.canRequestPackageInstalls()) {
                startActivity(
                    Intent(
                        Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES,
                        Uri.parse("package:$packageName")
                    )
                )
            }
        }
        if (Build.VERSION.SDK_INT >= 23) { // 마시멜로(안드로이드 6.0) 이상 권한 체크
            TedPermission.with(mContext)
                .setPermissionListener(permissionlistener)
                .setRationaleMessage("앱을 이용하기 위해서는 접근 권한이 필요합니다")
                .setDeniedMessage("앱에서 요구하는 권한설정이 필요합니다...\n [설정] > [권한] 에서 사용으로 활성화해주세요.")
                .setPermissions(
                    Manifest.permission.ACCESS_FINE_LOCATION,
                    Manifest.permission.ACCESS_COARSE_LOCATION
                )
                .check()
        } else {
            initView()
        }
    }

    private fun initView() {
        Handler().postDelayed({
            startActivity(Intent(this, MapsActivity::class.java))
            finish()  // close this activity
        }, SPLASH_TIME_OUT)
    }

}

 

import android.app.Activity
import android.os.Build
import android.widget.Toast

class BackPressHandler(private val activity: Activity) {
    private var backKeyPressedTime: Long = 0
    private var toast: Toast? = null
    fun onBackPressed() {
        if (isAfter2Seconds) {
            backKeyPressedTime = System.currentTimeMillis()
            // 현재시간을 다시 초기화
            toast = Toast.makeText(
                activity,
                "\'뒤로\'버튼을 한번 더 누르면 종료됩니다.",
                Toast.LENGTH_SHORT
            )
            toast!!.show()
            return
        }
        if (isBefore2Seconds) {
            appShutdown()
            toast!!.cancel()
        }
    }

    // 2초 지났을 경우
    private val isAfter2Seconds: Boolean
        private get() = System.currentTimeMillis() > backKeyPressedTime + 2000

    // 2초가 지나지 않았을 경우
    private val isBefore2Seconds: Boolean
        private get() = System.currentTimeMillis() <= backKeyPressedTime + 2000

    private fun appShutdown() {
        // 홈버튼 길게 누르면 나타나는 히스토리에도 남아있지 않고 어플 종료
        if (Build.VERSION.SDK_INT >= 21) activity.finishAndRemoveTask() else activity.finish()
        System.exit(0)
    }
}

 

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity

class MapsActivity : AppCompatActivity() {
    private var backPressHandler: BackPressHandler? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_maps)

        backPressHandler = BackPressHandler(this); // 뒤로 가기 버튼 이벤트
    }

    override fun onBackPressed() {
        backPressHandler!!.onBackPressed()
    }
}

 

var a = readLine()?.capitalize()
// ?. : 안전 호출 연산자


var a = readLine()!!.capitailize()
// !! : non-null 단언 연산자. null이 될 수 없다는 것을 단언하는 연산자다.

 

블로그 이미지

Link2Me

,
728x90

네이버 지도를 이용하기 위해서는 네이버 클라우드 플랫폼에 환경 설정 정보를 등록해야 한다.

 

https://www.ncloud.com/ 에 로그인 하고 나서 결제 연동 정보 등을 등록하고 이용하고자 하는 API를 선택한다.

 

기본 초기 설정 정보는 https://imweb.me/faq?mode=view&category=29&category2=37&idx=48381 참조하시라.

 

 

 

 

 

 

Web URL 또는 Android 앱 패키지 명을 등록하고 저장하면 앱 또는 Android에서 naver map aip를 이용할 수 있다.

 

 

 <meta-data
   android:name="com.naver.maps.map.CLIENT_ID"
   android:value="클라이언트값" />

 

Client ID 값을 복사하여 클라이언트값에 붙여넣기한다.

 

네이버 지도 소개

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

 

네이버지도 데모파일

https://github.com/navermaps/android-map-sdk

 

오픈 소스를 활용한 지도 구글 서비스 개발 (서버 : Node.js)

https://github.com/kairo96/

이 자료는 책을 구입해서 같이 봐야 이해하기 쉽고 도움된다.

http://www.kyobobook.co.kr/product/detailViewKor.laf?ejkGb=KOR&mallGb=KOR&barcode=9791185890968&orderClick=LAG&Kc=

 

블로그 이미지

Link2Me

,