728x90

AsyncTask 를 Class 화하여 사용하는 방법이다.


private void getJSONFromAsyncHttpClient() {
    Uri.Builder builder = new Uri.Builder()
            .appendQueryParameter("uID", Value.encrypt(Value.getPhoneUID(context)))
            .appendQueryParameter("userID", Value.encrypt(userID))
            .appendQueryParameter("searName", Value.encrypt(gubun))
            .appendQueryParameter("searValue", Value.encrypt(tmpStr))
            .appendQueryParameter("keyword", Value.encrypt(Value.URLkey()));
    String params = builder.build().getEncodedQuery();

    AsyncHttpComm asyncHttpComm = new AsyncHttpComm();
    try {
        String result = asyncHttpComm.execute(Value.IPADDRESS + "/SearchList.php", params).get();
        showJSONList(result);
    } catch (InterruptedException e) {
        e.printStackTrace();
    } catch (ExecutionException e) {
        e.printStackTrace();
    }
} 


위 코드를 보면 AsyncHttpComm Class를 호출하고 결과 문자열을 String result 로 받고 있다.

그리고 showJSONList(result) 에 결과를 넣어서 서버로 받은 메시지를 파싱하여 RecyclerView 에서 처리하면 된다.


그럼 이제 AsyncHttpComm Class 는 어떻게 되어 있는지 보자.

public class AsyncHttpComm extends AsyncTask<String, Void, String> {

    @Override
    protected String doInBackground(String... params) {
        try {
            return PHPComm.getJson(params[0], params[1]);
        } catch (Exception e) {
            return new String("Exception: " + e.getMessage());
        }
    }
} 


String 변수를 받아서 백그라운드 처리하고, String 결과를 받고 있다.


또 메소드 처리되어 있는 PHPComm Class 는 어떻게 만들어져 있나 보자.

세션에 대한 내용이 적혀 있는데 별 의미가 없는 거 같다.

내가 아직 잘 몰라서 그런지 안드로이드와 PHP 간 통신하는데 있어서 세션값을 물고 다니면서 처리하는 방식으로 구현하지 않아도 통신하는데 전혀 문제가 되지 않는다.

스마트폰 고유장치 값, 키 값을 이용하여 PHP 에서 해킹시도 자체를 방지할 수 있다.


public class PHPComm extends Activity {

    // serverURL : JSON 요청을 받는 서버의 URL
    // postParams : POST 방식으로 전달될 입력 데이터
    // 반환 데이터 : 서버에서 전달된 JSON 데이터
    public static String getJson(String serverUrl, String postParams) throws Exception {

        BufferedReader bufferedReader = null;
        try {  
            Thread.sleep(100);
            URL url = new URL(serverUrl);  
            HttpURLConnection conn = (HttpURLConnection) url.openConnection();
            // 세션 쿠키 전달
            String cookieString = CookieManager.getInstance().getCookie(Value.IPADDRESS);
            
            StringBuilder sb = new StringBuilder();  

            if(conn != null){ // 연결되었으면
                //add request header
                conn.setRequestMethod("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.setConnectTimeout(10000);
                conn.setReadTimeout(10000);
                conn.setUseCaches(false);
                conn.setDefaultUseCaches(false);
                conn.setDoOutput(true); // POST 로 데이터를 넘겨주겠다는 옵션
                conn.setDoInput(true);

                // Send post request
                DataOutputStream wr = new DataOutputStream(conn.getOutputStream());
                wr.writeBytes(postParams);
                wr.flush();
                wr.close();

                int responseCode = conn.getResponseCode();
                System.out.println("GET Response Code : " + responseCode);        
                if(responseCode == HttpURLConnection.HTTP_OK){ // 연결 코드가 리턴되면
                    bufferedReader = new BufferedReader(new InputStreamReader(conn.getInputStream()));
                    String json;
                    while((json = bufferedReader.readLine())!= null){
                        sb.append(json + "\n");
                    }      
                }
                bufferedReader.close();
            }
            System.out.println("PHP Comm Out : " + sb.toString());
            return sb.toString().trim();  
            // 수행이 끝나고 리턴하는 값은 다음에 수행될 onProgressUpdate 의 파라미터가 된다
        } catch(Exception e){  
            return new String("Exception: " + e.getMessage());
        }   
 
    }
}


이 두개의 Class 를 만들고 나면 Library 를 사용하는 것만큼 편하게 Text 데이터를 수신하여 파싱하고 결과처리를 쉽게 할 수 있다.


http://www.masterqna.com/android/29387/asynctask-%EC%9D%98-get-%EB%A9%94%EC%86%8C%EB%93%9C-%EC%A7%88%EB%AC%B8

에 나온 내용을 보니

AsyncTask.get()은 UI thread를 block 한다고
AsyncTaks안에 onPostExecute()를 implement하고 Handle 하라고 나온다.
시간이 오래 걸리는 작업은 AsyncTask.get()을 사용하지 말자.



이런거 귀찮다?????

좀 더 간편하게 라이브러리 이용하여 하고 싶다면

앱 build.gradle 에 아래 라이브러리를 추가한다.

dependencies {
    implementation 'com.loopj.android:android-async-http:1.4.9'
}


코드는 아래와 같이 심플하다.


 private void getJSONFromAsyncHttpClient() {
    RequestParams params = new RequestParams();
    params.put("keyword", Value.encrypt(Value.URLkey()));
    params.put("uID", Value.encrypt(Value.getPhoneUID(context)));
    params.put("userID", Value.encrypt(userID));
    params.put("searName", Value.encrypt(gubun));
    params.put("searValue", Value.encrypt(tmpStr));

    AsyncHttpClient client = new AsyncHttpClient();
    client.post(Value.IPADDRESS + "/SearchList.php", params, new TextHttpResponseHandler() {
        @Override
        public void onFailure(int statusCode, Header[] headers, String responseString, Throwable throwable) {
            System.out.println("statusCode : " + statusCode);
            Toast.makeText(Search.this, "서버와의 통신 중 에러가 발생했습니다.", Toast.LENGTH_SHORT).show();
        }

        @Override
        public void onSuccess(int statusCode, Header[] headers, String responseString) {
            System.out.println("statusCode : " + statusCode);
            //System.out.println("responseString : " + responseString);
            showJSONList(responseString);
        }
    });
}


라이브러리가 모든 폰에서 잘 동작되는지 여부를 확인한 것은 아니라서 표준 HttpUrlConnection 을 이용하여 통신하는 걸 구현하여 사용중이다.

Asynchronous HTTP Client 라이브러리에 대한 내용은 http://link2me.tistory.com/1493 참조하면 된다.


도움이 되셨다면 공감 그리고 ... 클릭 해 주시면 큰 힘이 됩니다.

블로그 이미지

Link2Me

,
728x90

Android background 실행 제한

https://developer.android.com/about/versions/oreo/background?hl=ko 의 내용을 발췌 요약했다.


Android 8.0는 사용자 경험을 개선하기 위해 백그라운드에서 실행되면서 앱의 동작을 제한한다.
잘 동작하던 발신자정보 팝업창이 Android 8.0 에서 잠시 보여주고 사라져 버린다.
- 백그라운드 서비스 제한: 앱이 유휴 상태인 경우 백그라운드 서비스의 사용이 제한된다.
  마치 앱이 서비스의 Service.stopSelf() 메서드를 호출한 것처럼 시스템이 앱의 백그라운드 서비스를 중지시킨다.
  . 액티비티가 시작되거나 일시 중지되거나 상관없이 보이는 액티비티가 있는 경우 앱이 포그라운드에 있는 것으로 간주
  . 포그라운드 서비스가 있는 경우 앱이 포그라운드에 있는 것으로 간주
- 브로드캐스트 제한: 앱이 암시적 브로드캐스트에 등록하기 위해 자체 매니페스트를 사용할 수 없다.
  그렇지만 여전히 앱이 런타임에 브로드캐스트에 등록할 수 있으며,
  특정 앱을 대상으로 하는 명시적 브로드캐스트에 등록하기 위해 매니페스트를 사용할 수 있다.
앱은 JobScheduler 작업을 사용하여 이러한 제한을 해결할 수 있다.
Android 8.0에서는 JobScheduler에 대한 여러 가지 개선 사항을 제공하며 이를 통해 서비스와 브로드캐스트 수신기를 예약된 작업으로 쉽게 바꿀 수 있다.

앱이 허용 목록에 있는 동안에는 제한없이 서비스를 시작
- 우선순위가 높은 Firebase 클라우드 메시징(FCM) 메시지 처리
- SMS/MMS 메시지와 같은 브로드캐스트 수신
- 알림에서 PendingIntent 실행

Android 8.0에서는 새 서비스를 포그라운드에서 시작하는 새로운 메서드 Context.startForegroundService()를 소개
- 시스템이 서비스를 생성한 후, 앱은 5초 이내에 해당 서비스의 startForeground() 메서드를 호출하여 새 서비스의 알림을 사용자에게 표시해야 한다.
- 앱이 이 시간 한도 내에 startForeground()를 호출하지 않으면 시스템이 서비스를 중단하고 이 앱을 ANR로 선언한다.
- 오디오를 재생하는 서비스는 항상 포그라운드 서비스여야 한다.
  startService() 대신 NotificationManager.startServiceInForeground()를 사용하여 서비스를 생성한다.


- Foreground service 의 경우 반드시 noficiation 을 제공해야 한다.
  Foreground service 는 보통 Music Player 와 같이 interactive 한 service 에 사용한다.
  foreground 는 startForeground() 로 시작시킨다. 이 때 Notification ID 와 notification을 함께 전달한다.
  parameter로 stopForeground() 로 stop 시키는데 parameter 로 notification 도 함께 제거할지를 boolean 값으로 넘기게 된다.

// 서비스를 실행할 때
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
    context.startForegroundService(new Intent(context, ServedService.class));
} else {
    context.startService(new Intent(context, ServedService.class));
}

// 서비스 파일 내에서
@Override
public void onCreate() {
    super.onCreate();
    startForeground(1,new Notification()); // 추가 (9.0에서는 변경해야 함)
}

@Override
public void onDestroy() {
    // 서비스가 종료될 때 실행
    super.onDestroy();
    stopForeground(true);
}


2019.4.6일 추가 내용


Android 9.0 에서는 아래와 같이 변경해야 제대로 동작한다.

AndroidManifest.xml 파일에 한줄 추가해야 한다.

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


startForeground(1,new Notification()); // 대신에 아래 코드로 변경한다.


        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
            startMyOwnForeground();

    @RequiresApi(api = Build.VERSION_CODES.O)
    private void startMyOwnForeground() {
        String NOTIFICATION_CHANNEL_ID = "com.link2me.android.Phone";
        String channelName = "Incoming Calling Service";
        NotificationChannel chan = new NotificationChannel(NOTIFICATION_CHANNEL_ID, channelName, NotificationManager.IMPORTANCE_NONE);
        chan.setLightColor(Color.BLUE);
        chan.setLockscreenVisibility(Notification.VISIBILITY_PRIVATE);
        NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
        assert manager != null;
        manager.createNotificationChannel(chan);

        NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID);
        Notification notification = notificationBuilder.setOngoing(true)
                .setSmallIcon(R.drawable.icon)
                .setContentTitle("App is running in background")
                .setPriority(NotificationManager.IMPORTANCE_MIN)
                .setCategory(Notification.CATEGORY_SERVICE)
                .build();
        startForeground(2, notification);
    }
 


전화번호 액세스 제한

https://developer.android.com/about/versions/pie/android-9.0-changes-all?hl=ko




블로그 이미지

Link2Me

,
728x90

안드로이드 8.0 에서 서비스를 실행할 때 아래코드를 추가해줘야 에러가 발생하지 않는다.


// 서비스를 실행할 때
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
    context.startForegroundService(new Intent(context, ServedService.class));
} else {
    context.startService(new Intent(context, ServedService.class));
}

// 서비스 파일 내에서
@Override
public void onCreate() {
    super.onCreate();
    startForeground(1,new Notification()); // 추가
}

@Override
public void onDestroy() {
    // 서비스가 종료될 때 실행
    super.onDestroy();
    stopForeground(true);
}



블로그 이미지

Link2Me

,
728x90

안드로이드 Notification Helper 자료를 검색하고 내용을 보강하고 적어둔다.

안드로이드 8.0 이상(targetSdkVersion 26 이상)에서도 동작되는 코드다.

Noti 메시지를 띄우는createNotification(String title, String message) 과 클릭하면 cancelNotification(Context ctx, int notifyId) 되는 걸 테스트했다.

핵심은 NotificationHelper Class 만드는 것이고 나머지는 간단하게 사용해본 것이다.


 public class NotificationHelper {
    private Context mContext;
    private NotificationManager notificationManager;
    public static final String NOTIFICATION_CHANNEL_ID = "10001";

    public NotificationHelper(Context context) {
        mContext = context;
    }

    /**
     * Create and push the notification
     */
    public void createNotification(String title, String message) {
        Intent resultIntent = new Intent(mContext , ShowNotification.class);
        resultIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);

        PendingIntent resultPendingIntent = PendingIntent.getActivity(mContext,
                0 /* Request code */, resultIntent,
                PendingIntent.FLAG_UPDATE_CURRENT);

        NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(mContext,NOTIFICATION_CHANNEL_ID);
        mBuilder.setSmallIcon(R.mipmap.ic_launcher);
        mBuilder.setContentTitle(title)
                .setContentText(message)
                .setAutoCancel(false) //클릭하게 되면 사라지도록...
                .setSound(Settings.System.DEFAULT_NOTIFICATION_URI)
                .setVibrate(new long[] { 1000, 1000, 1000 }) //노티가 등록될 때 진동 패턴
                .setContentIntent(resultPendingIntent);

        notificationManager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);

        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O){ // 8.0 이상
            int importance = NotificationManager.IMPORTANCE_HIGH;
            String channelName = "NOTIFICATION_CHANNEL_NAME";
            NotificationChannel notificationChannel = new NotificationChannel(NOTIFICATION_CHANNEL_ID, channelName, importance);
            notificationChannel.enableLights(true);
            notificationChannel.setLightColor(Color.RED);
            notificationChannel.enableVibration(true);
            notificationChannel.setVibrationPattern(new long[]{100, 200, 300, 400, 500, 400, 300, 200, 400});
            assert notificationManager != null;
            mBuilder.setChannelId(NOTIFICATION_CHANNEL_ID);
            notificationManager.createNotificationChannel(notificationChannel);
        }
        assert notificationManager != null;
        notificationManager.notify(0 /* Request Code */, mBuilder.build());
    }

    public void callSettingNotification(){
        if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O){
            Intent intent = new Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS);
            intent.putExtra(Settings.EXTRA_APP_PACKAGE,mContext.getPackageName());
            intent.putExtra(Settings.EXTRA_CHANNEL_ID,NOTIFICATION_CHANNEL_ID);
            mContext.startActivity(intent);
        }
    }

    public void cancelNotification(Context ctx, int notifyId) {
        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O){
            NotificationManager mNotificationManager =
                    (NotificationManager) ctx.getSystemService(Context.NOTIFICATION_SERVICE);
            mNotificationManager.deleteNotificationChannel(NOTIFICATION_CHANNEL_ID);
        } else {
            NotificationManager notificationManager = (NotificationManager) ctx.getSystemService(Context.NOTIFICATION_SERVICE);
            notificationManager.cancel(notifyId);
        }
    }
}


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

notificationmanager.zip


PendingIntent

PendingIntent는 Intent를 포함하는 인텐트로, 목적은 본인 앱이 아닌 외부 앱(Notification, Alarm 등)에게 권한을 줘서 본인 앱을 실행할 수 있도록 허락하는 것이다.


A PendingIntent is a token that you give to a foreign application (e.g. NotificationManager, AlarmManager, Home Screen AppWidgetManager, or other 3rd party applications), which allows the foreign application to use your application's permissions to execute a predefined piece of code.


pending : 미결의, ~를 기다리는 동안


Intent의 기본 개념은 특정 컴포넌트(Activity, Service, Broadcast Receiver, Content Provider)를 실행시키는 메시지라는 것이다.

Intent intent = new Intent(MainActivity.this, NewActivity.class);

startActivity(intent);

그런데 PendingIntent는 생성자가 없고 다음 세 개의 메소드들에 의해서 객체가 생성된다.
ㆍgetActivity(Context, int, Intent, int)
ㆍgetBroadcast(Context, int, Intent, int)
ㆍgetService(Context, int, Intent, int)

- 액티비티를 시작할 인텐트를 생성하기 위해선 PendingIntent.getActivity()를 사용한다.
- 서비스를 시작할 인텐트를 생성하기 위해서는 PendingIntent.getService()를 사용한다.
- BroadcastReceiver를 시작할 인텐트를 생성하기 위해서는 PendingIntent.getBroadcast()를 사용한다.


Intent intent = new Intent(context, MainActivity.class);
PendingIntent pendIntent = PendingIntent.getActivity(context, 0 , intent, PendingIntent.FLAG_UPDATE_CURRENT);

- 정상적인 Activity lifecycle을 태우기 위해선, PendingIntent 생성시 requestCode를 반드시 넣어야한다.(대부분 default값으로 0을 넣는다)

- FLAG_CANCEL_CURRENT : 이전에 생성한 PendingIntent는 취소하고 새로 만든다.
- FLAG_IMMUTABLE : 생성된 PendingIntent 는 수정 불가능하도록 한다.
- FLAG_NO_CREATE : 생성된 PendingIntent 를 반환한다.(재사용 가능), 이미 생성된 PendingIntent가 없다면 null를 return 한다.
- FLAG_ONE_SHOT : 해당 Flag로 생성한 PendingIntent 는 일회성이다.
- FLAG_UPDATE_CURRENT : 이미 생성된 PendingIntent가 존재하면 해당 Intent의 Extra Data만 변경한다.


알람 확인 및 취소 게시글 : https://migom.tistory.com/10





참고사이트

https://medium.com/cr8resume/notification-in-android-8-0-oreo-implementing-notification-channels-d65b0f81ca50


https://www.androidauthority.com/android-8-0-oreo-app-implementing-notification-channels-801097/


http://gun0912.tistory.com/77

블로그 이미지

Link2Me

,
728x90



안드로이드 스튜디오에서 Interface CallbackEvent를 생성하고 나서 EventRegistration Class 에서 CallbackEvent  변수를 선언하고 생성자 만들기를 하면 자동으로 생성자가 만들어지는 걸 볼 수 있다.


public interface CallbackEvent {
    // 1 Step 인터페이스 정의
    public void callbackMethod();
}

 public class EventRegistration {
    // 2 Step 변수 선언
    CallbackEvent callbackEvent;

    // 3 Step 생성자 생성
    public EventRegistration(CallbackEvent callbackEvent) {
        System.out.println("2. EventRegistration Constructor");
        this.callbackEvent = callbackEvent;
    }

    public void doWork(){
        System.out.println("4. doWork 메소드");
        callbackEvent.callbackMethod();
    }
}

 public class MainActivity extends AppCompatActivity {

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

        System.out.println("1. MainActivity");
        CallbackEvent callbackEvent = new CallbackEvent() {
            @Override
            public void callbackMethod() {
                System.out.println("5. call callback method from callee");
            }
        };

        EventRegistration eventRegistration = new EventRegistration(callbackEvent);
        System.out.println("3. eventRegistration.doWork()");
        eventRegistration.doWork();
    }
}


생성자 생성과 동시에 EventRegistration(CallbackEvent callbackEvent) 를 지정하는 방법과

별도 setCallbackEvent(CallbackEvent callbackEvent) 를 하는 방법으로 구분해서 해보고 있다.


public interface CallbackEvent {
    // 1 Step 인터페이스 정의
    public void callbackMethod();
}

 public class EventRegistration {
    Context mContext;
    // 2 Step 변수 선언
    CallbackEvent callbackEvent;

    // 3 Step 생성자 선언
    public EventRegistration(Context context) {
        System.out.println("3. EventRegistration Constructor");
        mContext = context;
    }

    // 4 Step CallbackEvent Set
    public void setCallbackEvent(CallbackEvent callbackEvent){
        System.out.println("5. setCallbackEvent");
        this.callbackEvent = callbackEvent;
    }

    public void doWork(){
        System.out.println("7. doWork 메소드 응답");
        callbackEvent.callbackMethod();
    }
}

 public class MainActivity extends AppCompatActivity {
    Context context;

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

        System.out.println("1. MainActivity 실행");

        System.out.println("2. EventRegistration 생성자 호출");
        EventRegistration eventRegistration = new EventRegistration(context);

        System.out.println("4. setCallbackEvent 연동");
        eventRegistration.setCallbackEvent(new CallbackEvent() {
            @Override
            public void callbackMethod() {
                System.out.println("8. call callback method from callee");
            }
        });

        System.out.println("6. eventRegistration.doWork() 실행");
        eventRegistration.doWork();
    }
}


예제3.

안드로이드에서 보편적으로 사용되는 예제이다.

public interface CallbackEvent {
    void callbackMethod();
}

import android.content.Context;

public class NewClass {
    private Context mContext;
    private CallbackEvent callbackEvent;

    public NewClass(Context mContext) {
        System.out.println("2. NewClass Constructor");
        this.mContext = mContext;
    }

    public void setCallbackEvent(CallbackEvent callbackEvent){
        System.out.println("3. setCallbackEvent");
        this.callbackEvent = callbackEvent;
    }

    public void doWork(){
        //Do somthing...

        //call back main
        callbackEvent.callbackMethod();
        System.out.println("5. doWork 메소드 응답");
    }
}

import android.content.Context;
import android.os.Bundle;

import androidx.appcompat.app.AppCompatActivity;

public class MainActivity extends AppCompatActivity implements CallbackEvent {
    Context mContext;
    NewClass newClass;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mContext = MainActivity.this;
        System.out.println("1. MainActivity 실행");
        doSomething();
    }

    private void doSomething(){
        newClass = new NewClass(mContext);
        newClass.setCallbackEvent(this);
        newClass.doWork();
        System.out.println("6. newClass.doWork()");
    }

    @Override
    public void callbackMethod() {
        System.out.println("4. callbackMethod from callee.");
    }
}



블로그 이미지

Link2Me

,
728x90

초급 단계를 벗어나고자 Interface 에 대해 공부하고 있는데 생각만큼 이해가 잘 안된다.



https://guides.codepath.com/android/Creating-Custom-Listeners 를 보고 테스트 해본 걸 적어둔다.


import android.view.View;

class MyCustomObject {
    // Step 1 - 인터페이스 정의
    public interface MyCustomObjectListener {
        // These methods are the different events and
        // need to pass relevant arguments related to the event triggered
        public void onObjectReady(String title);
        // or when data has been loaded
        void onItemClick(View view, int position);
    }

    // Step 2 - This variable represents the listener passed in by the owning object
    // The listener must implement the events interface and passes messages up to the parent.
    private MyCustomObjectListener listener;

    // Constructor where listener events are ignored
    public MyCustomObject() {
        // set null or default listener or accept as argument to constructor
        this.listener = null;
    }

    // Step 3 - Assign the listener implementing events interface that will receive the events
    public void setCustomObjectListner(MyCustomObjectListener listner){
        this.listener = listner;
    }
}


import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.View;

public class MyParentActivity extends AppCompatActivity {

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

        // Create the custom object
        MyCustomObject childObject = new MyCustomObject();

        // Step 4 - Setup the listener for this object
        childObject.setCustomObjectListner(new MyCustomObject.MyCustomObjectListener() {
            @Override
            public void onObjectReady(String title) {

            }

            @Override
            public void onItemClick(View view, int position) {

            }

        });
    }
}




Recyclerview 에서 Apdater를 만들어서 Interface 를 통해 데이터를 주고 받은 걸 해보고 싶은데 이해를 못한 부분이 있는지 예제를 보고 따라해보고 막상 적용해보려고 하면 막힌다.


public void setOnItemClickListener(OnItemClickListener listener){
   this.listener = listener;
}

를 통해서 데이터를 주고 받은 걸 해보고 싶은데 전혀 동작이 안된다.


생성자에 값을 넘겨서 처리하면 동작되는데 분리해서 접근하면 안되네.

뭔가 가능한 방법이 있을 거 같은데 실력이 미천해서 완벽한 해결을 못하고 Inner Class 로 처리를 하고 있다.

아래와 같이 생성자를 통해서 값을 넘기면 원하는 결과를 얻을 수는 있다.


public class StaffListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>  {
    public interface OnItemClickListener {
        void onItemClick(View view, int position);
    }

    public OnItemClickListener listener;

    public StaffListAdapter(Context mContext, ArrayList<Group_Item> items, OnItemClickListener listener) {
        this.mContext = mContext;
        IvList = items;
        this.listener = listener;
    }

}

public class StaffList extends AppCompatActivity {
    Context context;
    private ArrayList<Group_Item> staffItemList = new ArrayList<>();
    RecyclerView.Adapter staffListAdapter;

    staffListAdapter = new StaffListAdapter(this, staffItemList, new StaffListAdapter.OnItemClickListener() {
        @Override
        public void onItemClick(View view, int position) {
            if(staffItemList.get(position).getIsFolder().equals("1")){
            } else if(staffItemList.get(position).getIsFolder().equals("0")){
            }
        }
    });
    listView.setAdapter(staffListAdapter); // 어댑터를 리스트뷰에 세팅
}

블로그 이미지

Link2Me

,
728x90

두개의 테이블에서 불일치하는 데이터를 찾는 SQL 명령으로 idx 는 index를 타는 칼럼이다.

select * from favoriteBook where staffidx not in (select idx from member);

select * from FavoriteBook where staffidx not in (select idx from SYS_MEMBER);

 

불일치 데이터를 찾아서 지우는 SQL 문

delete from FavoriteBook where staffidx not in (select idx from member);

delete from FavoriteBook where staffidx not in (select idx from SYS_MEMBER);

 

select * from FavoriteBook where myidx not in (select idx from SYS_MEMBER);
delete from FavoriteBook where myidx not in (select idx from SYS_MEMBER);

 

누락된 데이터 찾기

SELECT b.fieldB, a.fieldA FROM tableA a RIGHT OUTER JOIN tableB b ON a.fieldA = b.fieldB WHERE a.fieldA IS NULL;

 

불일치 데이터 찾기

두 테이블의 uid는 서로 같고 특정 필드 업데이트에 따른 불일치가 되는 데이터 찾기

즉 백업 이후에 변경된 데이터 찾기 같은 셈이다.

select a.uid,a.direct,b.direct,a.is_checking,b.is_checking from data_0218 a, data_0220 b where a.uid=b.uid and a.is_direct=5 and a.is_checking=0 and b.is_checking NOT IN(0);

 

 

 

 

 

'SQL' 카테고리의 다른 글

테이블명 변경  (0) 2019.03.11
MySQL 사용자 권한 부여  (0) 2018.11.26
Toad for MySQL - Database Diagram  (0) 2017.06.02
[강좌] 한방에 개념잡는 SQL 조인  (0) 2017.02.26
[MySQL] 정규식을 활용한 검색 (REGEXP)  (0) 2016.12.11
블로그 이미지

Link2Me

,
728x90

서버에 있는 이미지를 다운로드하여 Glide 로딩 라이브러리를 이용하여 ImageView 에 보여주는 코드를 테스트하고 적어둔다.


앱 build.gradle

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'com.android.support:appcompat-v7:27.1.1'
    implementation 'com.android.support.constraint:constraint-layout:1.1.2'
    implementation 'com.github.bumptech.glide:glide:3.8.0'
    implementation 'gun0912.ted:tedpermission:2.0.0'
}



public class MainActivity extends AppCompatActivity {
    Context context;
    String croppedFolderName = "/DCIM/PhotoTemp";
    File outputFile = null;
    String outputFileName="myprofile.jpg";
    private ImageView imageView;

    private BackPressHandler backPressHandler;
    DownloadFileFromURL downloadFileAsyncTask;
    ProgressBar progressBar;

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

        @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();

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

        NonSecretApp_Setting(); // 출처를 알 수 없는 앱 설정 화면 띄우기
        checkPermissions();
    }

    private void checkPermissions() {
        if (Build.VERSION.SDK_INT >= 23){ // 마시멜로(안드로이드 6.0) 이상 권한 체크
            TedPermission.with(context)
                    .setPermissionListener(permissionlistener)
                    .setRationaleMessage("이미지를 다루기 위해서는 접근 권한이 필요합니다")
                    .setDeniedMessage("앱에서 요구하는 권한설정이 필요합니다...\n [설정] > [권한] 에서 사용으로 활성화해주세요.")
                    .setPermissions(new String[] { Manifest.permission.READ_EXTERNAL_STORAGE,
                            Manifest.permission.WRITE_EXTERNAL_STORAGE,
                            Manifest.permission.CAMERA})
                    .check();

        } else {
            initView();
        }
    }

    private void initView() {
        imageView = (ImageView) findViewById(R.id.cropImageView);
        Button btn_downloadPhoto = (Button) findViewById(R.id.btn_PhotoDownload);
        btn_downloadPhoto.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                if (NetworkHelper.checkConnection(context)) { // 인터넷 연결 체크
                    Log.d("Photo", "Photo Upload Task Start");
                    String PhotoURL = Value.IPADDRESS + "/myphoto.jpg";
                    downloadFileAsyncTask = new DownloadFileFromURL();
                    downloadFileAsyncTask.execute(PhotoURL);
                } else {
                    Toast.makeText(context, "인터넷 연결을 확인하세요", Toast.LENGTH_LONG).show();
                }
            }
        });

    }

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

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

        @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 = new File(Environment.getExternalStorageDirectory() + croppedFolderName);
                if (! path.exists())
                    path.mkdirs(); // 디렉토리가 없으면 생성
                outputFile = new File(path, outputFileName);
                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) {
                        //Very bad things just happened... handle it
                    }
                }
                if (fos != null) {
                    try {
                        fos.close();
                    }
                    catch(IOException ioex) {
                        //Very bad things just happened... handle it
                    }
                }
            }
            return null;
        }

        protected void onProgressUpdate(Integer... progress) {
            super.onProgressUpdate(progress);
            // 백그라운드 작업의 진행상태를 표시하기 위해서 호출하는 메소드
        }

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

                String imageUri = outputFile.getAbsolutePath();
                Glide.with(context).load(imageUri).into(imageView);
            } else {
                Toast.makeText(getApplicationContext(), "다운로드 에러", Toast.LENGTH_LONG).show();
            }
        }

        protected void onCancelled() {
            // cancel메소드를 호출하면 자동으로 호출되는 메소드
            progressBar.setProgress(0);
        }
    }

    private void NonSecretApp_Setting() {
        if(Build.VERSION.SDK_INT >= 26)  { // 출처를 알 수 없는 앱 설정 화면 띄우기
            PackageManager packageManager = getPackageManager();
            boolean isTrue = packageManager.canRequestPackageInstalls();

            if (!packageManager.canRequestPackageInstalls()) {
                AlertDialog.Builder b = new AlertDialog.Builder(this, android.R.style.Theme_DeviceDefault_Light_Dialog);
                b.setTitle("알림");
                b.setMessage("보안을 위해 스마트폰 환경설정의 '앱 설치 허용'을 설정해 주시기 바랍니다.설정화면으로 이동하시겠습니까?");
                b.setCancelable(false);
                b.setPositiveButton("설정하기", new DialogInterface.OnClickListener() {
                    public void onClick(DialogInterface dialog, int which) {
                        Intent intent = new Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES);
                        intent.setData(Uri.parse("package:" + getPackageName()));
                        startActivity(intent);
                    }
                });

                b.setNegativeButton("건너띄기", new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {

                    }
                });
                b.show();
            }
        }
    }

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


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

imagedownload.zip


이미지 CROP 처리 구현을 깔끔하게 하는 방법을 해보고자 하는데 좀 더 노력이 필요한 듯....

블로그 이미지

Link2Me

,
728x90

마시멜로 버전부터는 권한을 일반 권한(Normal Permission)과 위험 권한(Dangerous Permission)으로 나누었으며, 위험 권한의 경우에는 앱이 실행될 때 사용자에게 권한 부여할 것인지 물어보도록 변경되었다.

대표적인 위험권한은 위치, 카메라, 마이크, 연락처, 전화, 문자, 일정, 센서 등이다.


TedPermission 라이브러리를 이용하여 권한체크 하는 방법으로 간단히 이용할 수 있어 편하다.

제작자 블로그 : http://gun0912.tistory.com/


신규 스마트폰은 SDK 버전이 23 이라고 봐도 무방하다.


targetSdkVersion 23 이상으로 설정한 경우
dependencies {
    implementation 'gun0912.ted:tedpermission:2.0.0'
}
추가한다.


사용 예제

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

public class MainActivity extends AppCompatActivity {
    Context context;

    PermissionListener permissionlistener = new PermissionListener() {
        @Override
        public void onPermissionGranted() {
            initView(); // 권한이 승인되었을 때 실행할 함수
        }

        @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();

        checkPermissions();
    }

    private void checkPermissions() {
        if (Build.VERSION.SDK_INT >= 23){ // 마시멜로(안드로이드 6.0) 이상 권한 체크
            TedPermission.with(context)
                    .setPermissionListener(permissionlistener)
                    .setRationaleMessage("이미지를 다루기 위해서는 접근 권한이 필요합니다")
                    .setDeniedMessage("앱에서 요구하는 권한설정이 필요합니다...\n [설정] > [권한] 에서 사용으로 활성화해주세요.")
                    .setPermissions(new String[] { Manifest.permission.READ_EXTERNAL_STORAGE,
                            Manifest.permission.WRITE_EXTERNAL_STORAGE,
                            Manifest.permission.CAMERA})
                    .check();

        } else {
            initView(); // 권한 승인이 필요없을 때 실행할 함수
        }
    }

    private void initView() {
        imageView = (ImageView) findViewById(R.id.croppedImageView);
        imageView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {                

            }
        });
    }

}


필요한 권한을 멀티로 지정해주면 된다.

'안드로이드 > Android 기능' 카테고리의 다른 글

Android – TextView autoLink attribute  (0) 2018.09.21
Android Notification  (0) 2018.08.24
Android View  (1) 2018.03.20
WindowManager 주요 Flag  (0) 2018.02.24
Transparent Activity floating/draggable on Homscreen  (0) 2018.01.30
블로그 이미지

Link2Me

,
728x90

Context mContext;
ImageView mImage = (ImageView) findViewById(R.id.croppedImageView);

String imageUri = Item.getPhoto();
if (imageUri.equals("")) { // 이미지가 없을 때
    int resourceId = R.drawable.photo_base;
    Glide.with(mContext).load(resourceId).into(mImage);
} else { // 이미지가 있을 때
    Glide.with(mContext).load(imageUri).into(mImage);
}


imageUri 는 실제 파일이 존재하는 경로, 예) http://www.abc.com/images/abc.jpg


폰의 폴더에 있는 이미지를 View 할 때

String croppedFolderName = "/DCIM/PhotoTemp";
File outputFile = null;
String outputFileName="myprofile.jpg";
private ImageView imageView;

File path = new File(Environment.getExternalStorageDirectory() + croppedFolderName);
if (! path.exists())
    path.mkdirs(); // 디렉토리가 없으면 생성
outputFile = new File(path, outputFileName);

Bitmap photoBitmap = BitmapFactory.decodeFile(outputFile.getAbsolutePath() );
imageView.setImageBitmap(photoBitmap);


Glide 로 View 하면

String imageUri = outputFile.getAbsolutePath();
Glide.with(context).load(imageUri).into(imageView);

블로그 이미지

Link2Me

,
728x90

FloatingActionButton(FAB)
UI 위에 떠 있는 동그라미 모양에 아이콘처럼 되어 사용되는 버튼이다.

build.gradle 추가 사항
dependencies {
    implementation 'com.google.android.material:material:1.1.0'
}

layout 예시
CoordinatorLayout 의 자식 레이아웃에 Floating Action Button 을 추가

<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout
    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">

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

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

    <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"
        app:backgroundTint="#60CEDC"
        app:srcCompat="@android:drawable/ic_menu_camera" />

</androidx.coordinatorlayout.widget.CoordinatorLayout>


activity 코드
// Floating Action Button을 리스트 뷰에 적용
FloatingActionButton fab = findViewById(R.id.fab);
fab.attachToListView(listView);
// 이벤트 적용
fab.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {

    }
});

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

Dynamic Layouts  (0) 2019.01.06
Android Fragment 기본 예제  (0) 2018.09.11
LinearLayout weight  (0) 2018.03.01
Android ViewFlipper(뷰플리퍼)  (0) 2017.05.02
Fragment 화면 이동  (0) 2017.03.26
블로그 이미지

Link2Me

,
728x90

Android Studio 에서 compileSdkVersion 을 바꾸면 아래와 같이 comile Library 버전을 맞춰줘야 에러가 없어진다.

그래서 찾아보기가 만만치 않아서 적어둔다.


compileSdkVersion 22
targetSdkVersion 22
compile 'com.android.support:support-v4:22.0.0'
compile 'com.android.support:appcompat-v7:22.0.0'

compileSdkVersion 23
targetSdkVersion 23
compile 'com.android.support:support-v4:23.0.0'
compile 'com.android.support:appcompat-v7:23.0.0'

compile 'com.android.support:appcompat-v7:23.1.1'

compile 'com.android.support:appcompat-v7:23.4.0'

compileSdkVersion 25
targetSdkVersion 25
compile 'com.android.support:appcompat-v7:25.1.0'

compileSdkVersion 26
targetSdkVersion 26
compile 'com.android.support:appcompat-v7:26.+'

블로그 이미지

Link2Me

,
728x90

https://developer.android.com/studio/build/application-id?hl=ko  을 읽어보면 내용이 잘 나온다.


개발할 때 동일한 앱을 두개 띄우고 서로간에 기능을 확인하고 싶다면

applicationId "com.tistory.link2me.asynchttpjson" 와 같이 applicationId 를 변경하면 또하나의 앱이 설치된다.

즉, 패키지명과 applicationId 를 다르게 하고서 개발 테스트를 하면 좋을 거 같다.


사무실과 집에 있는 PC 환경이 다르지만 src 폴더만 복사해서 코딩하면 되므로 편리하다.


집에 있는 PC build.gradle 및 Android Studio 3.1.3

apply plugin: 'com.android.application'

android {
    compileSdkVersion 26
    defaultConfig {
        applicationId "com.tistory.link2me.asynchttpjson"
        minSdkVersion 19
        targetSdkVersion 26
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'com.android.support:appcompat-v7:26.1.0'
    implementation 'com.android.support:design:26.1.0'
    implementation 'com.android.support.constraint:constraint-layout:1.1.2'
    implementation 'com.loopj.android:android-async-http:1.4.9'
    implementation 'com.github.bumptech.glide:glide:3.8.0'
    implementation 'com.android.support:recyclerview-v7:26.1.0'
    implementation 'com.android.support:cardview-v7:26.1.0'
} 


사무실 노트북 build.gradle 및 Android Studio 2.3.3

 apply plugin: 'com.android.application'

android {
    compileSdkVersion 26
    buildToolsVersion "26.0.0"

    defaultConfig {
        applicationId "com.tistory.link2me.asynchttpjson"
        minSdkVersion 19
        targetSdkVersion 26
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"

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

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile 'com.android.support:appcompat-v7:26.+'
    compile 'com.android.support.constraint:constraint-layout:1.0.2'
    compile 'com.loopj.android:android-async-http:1.4.9'
    compile 'com.github.bumptech.glide:glide:3.8.0'
    compile 'com.android.support:recyclerview-v7:24.2.0'
    compile 'com.android.support:cardview-v7:24.2.0'
}



조만간에 집에 있는 PC에서 Android Studio 2.3.3 과 3.1.3 을 동시에 이용할 수 있는 환경을 만들어서 테스트를 해볼 생각이다.

노트북에 있는 관련 파일을 모두 복사하고 환경 세팅을 서로 다르게 하면 둘 다 이용이 가능할 거 같다.


컴파일 속도나 인식속도는 Android Studio 3.1.3 이 엄청 빠르다.

그런데 기존 테스트 코드를 Import Moulde 하면 인식을 잘 못하는 경향이 있다. 버그(?)

그리고 Eclipse 코드를 읽어오기가 Android Studio 2.3.3 에서는 잘 되기 때문에 일단 코드를 읽은 다음에 Android Studio 환경에 맞게 수정하는 것이 편하기도 하고 노트북 속도 문제도 있어서 2.3.3 에서 업그레이드를 중지한 상태다.

블로그 이미지

Link2Me

,