728x90

출처 : stackoverflow.com/questions/58712648/getting-a-permission-error-even-after-permission-read-contacts-is-declared

public class MainActivity extends AppCompatActivity {

    // Request code for READ_CONTACTS. It can be any number > 0.
    private static final int PERMISSIONS_REQUEST_READ_CONTACTS = 100;

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

        // Read and show the contacts
        showContacts();
    }

    /**
     * Show the contacts in the ListView.
     */
    private void showContacts() {
        // Check the SDK version and whether the permission is already granted or not.
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && checkSelfPermission(Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED) {
            requestPermissions(new String[]{Manifest.permission.READ_CONTACTS}, PERMISSIONS_REQUEST_READ_CONTACTS);
            //After this point you wait for callback in onRequestPermissionsResult(int, String[], int[]) overriden method
        } else {
            // Android version is lesser than 6.0 or the permission is already granted.
            getContactNames();
        }
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, String[] permissions,
                                           int[] grantResults) {
        if (requestCode == PERMISSIONS_REQUEST_READ_CONTACTS) {
            if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                // Permission is granted
                showContacts();
            } else {
                Toast.makeText(this, "Until you grant the permission, we canot display the names", Toast.LENGTH_SHORT).show();
            }
        }
    }

    /**
     * Read the name of all the contacts.
     *
     * @return a list of names.
     */
    private void getContactNames() {
        Uri uri = ContactsContract.CommonDataKinds.Phone.CONTENT_URI;
        String[] projection    = new String[]          {ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME,
                ContactsContract.CommonDataKinds.Phone.NUMBER};

        Cursor people = getContentResolver().query(uri, projection, null,  null, null);

        int indexName = people.getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME);
        int indexNumber = people.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER);

        if(people.moveToFirst()) {
           do {
              String name   = people.getString(indexName);
              String number = people.getString(indexNumber);
              // add number to list
        // Do work...
           } while (people.moveToNext());
         }
         people.close();
    }
}
블로그 이미지

Link2Me

,
728x90

컴파일시 에러 메시지

Rejecting re-init on previously-failed class java.lang.Class<androidx.core.view.ViewCompat$2>


해결책

앱 build.gradle 에 추가

implementation 'androidx.core:core:1.5.0-alpha04'


https://developer.android.com/jetpack/androidx/releases/core?hl=ko 참조

블로그 이미지

Link2Me

,
728x90

필요한 걸 찾으려고 하니 어떤 게시글에 적혀 있는지 찾기 어려워서 별도로 적어둔다.


API28(Android 9) 이상을 사용하는 경우에는 추가로 고려할 사항이 있다.


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.MyPhone">

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

    <application
        android:allowBackup="false"
        android:icon="@drawable/icon"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:usesCleartextTraffic="true" 
<!-- http:// 통신일 경우에도 통신 가능하도록 -->

        android:theme="@style/AppTheme.NoActionBar">
        <activity android:name="com.link2me.android.MyPhone.Intro">
            <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>
    </application>
</manifest>

Android 7.0(Nougat / API 24)에서 Intent로 URI 파일 경로 전송시 "file://" 노출되어 있으면 FileUriExposedException 오류가 발생하게 되고 앱이 종료된다.
앱간 파일을 공유하려면 "file://" 대신 "content://"로 URI를 보내야 한다.
URI로 데이터를 보내기 위해선 임시 액세스 권한을 부여해야 하고 FileProvider를 이용해야 한다.

그리고 res/xml/provider_paths.xml 생성을 해야 한다.

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




앱 build.gradle 은 androidX 로 변경한다.
오래된 코드를 Android Studio 에 맞게 수정하기 위해서는 이 코드 변환 과정이 필요하다.

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])

    // implementation 'com.android.support:appcompat-v7:28.0.0'
    //implementation 'com.android.support.constraint:constraint-layout:1.1.3'

    implementation 'androidx.appcompat:appcompat:1.0.2'
    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
    implementation 'gun0912.ted:tedpermission:2.0.0'
    implementation 'com.android.volley:volley:1.1.1'
}




블로그 이미지

Link2Me

,
728x90

인코딩 문제인지 파이썬에서 개발했다고 하는 ANSI escape sequences 코드 적용된 장비 접속에서 회신이 제대로 오지 않는 문제로 골머리가 아프다.

PC/노트북용 Putty 에서는 정상적으로 처리되는데 Android 앱에서 구현한 코드로는 특정 명령어는 100% 회신이 오는데, 특정 명령어는 50% 성공율이 발생한다. 문제 해결을 위해서 C++ 유투브 강좌도 열심히 들어보고 있다.

Android NDK 를 이용한 방법으로 해결할 수 있지 않을까 생각과 더불어 NDK 처리 방법을 배우면 유용할 거 같아서다.

아직 문제 해결이 안된 상태인데 인코딩 문제인가 싶어서 관련 내용을 찾아보고 정리해둔다.


인코딩(encoding)은 문자셋을 컴퓨터가 이해할 수 있는 바이트와 매핑하는 규칙이다.
UTF-8은 모든 유니 코드 문자를 나타낼 수있는 멀티 바이트 인코딩이다.
ISO 8859-1은 첫 번째 256 유니 코드 문자를 나타낼 수있는 단일 바이트 인코딩이다.

ASCII(American Standard Code for Information Interchange)는 7비트로 엄격하게 제한되었으며, 표현 가능한 7개의 이진수로 비트 패턴을 사용함을 의미하는데, 10진수로는 0부터 127까지의 범위를 제공한다.
여기에는 32개의 보이지 않는 제어 문자가 있는데, 0부터 31까지의 범위에 있으며, 마지막 제어 문자는 DEL또는 delete이며, 이 제어문자는 127번에 할당되어 있다.
32번 부터 126번 까지는 공백 문자, 문장 부호, 라틴 문자, 숫자와 같은 눈에 보이는 문자로 구성되어 있다.
ASCII의 8번째 비트는 본디 오류 검출을 위한 패리티 비트로 활용했다.
오류 검사를 고려하지 않으면 0으로 그대로 둔다. ASCII에서 각 문자를 단일 바이트로 표현했음을 의미한다.
ㅇ 7 bit 코드 : 0x00 ~ 0x7F 까지 128개
ㅇ 첫 1bit 는 패러티 비트 : 에러 검출 목적

강세부호가 들어간 문자가 있는 다른 유럽어권의 문자를 ASCII 로 의사소통할 수 없어 ISO 8859 표준을 개발했다.
현재 ISO 8859 표준의 15가지 변형 표준이 있다.
ㅇ 8bit 코드 : 0x00 ~ 0xFF 까지 256개
ㅇ ASCII 에서 쓸 수 없는 문자들까지 포함
ㅇ 언어권에 따라 여러가지 변형
ㅇ 서유럽용 IOS-8859-1 을 가장 많이 씀


UTF-8

ㅇ 유니코드 인코딩 방식중 하나

ㅇ 영어는 1byte, 한글은 3byte


https://developer.android.com/training/articles/perf-jni#java 에 "자바 프로그래밍 언어는 UTF-16을 사용한다." 고 나온다. UTF-8 이 아니고???


The ANSI character set, also known as Windows-1252, has become a Microsoft proprietary character set;
it is a superset of ISO-8859-1 with the addition of 27 characters in locations that ISO designates for control codes.

ASCII 가  1byte 인코딩 방식이라고 하면, UTF-8 은 멀티방식 인코딩이지만 영문은 1byte 이므로 동일할 거 같고, ISO-8859-1 인코딩 방식이랑 차이가 없을거 같은 생각이 드는데 맞나??


아무튼 문제 해결을 위한 지식과 코드를 분석해보면서 해결책을 찾아야겠다.


Update : 2019.12.20

해결 결과 : 인코딩 문제가 아니라 흐름제어(Flow Control) 문제였다.


블로그 이미지

Link2Me

,
728x90

RecyclerView: No layout manager attached; skipping layout

라는 로그를 출력하고 있다.

무엇이 문제일까?


calendarView = this.findViewById(R.id.calendar);

바로 밑에다가


calendarView.setHasFixedSize(true);
calendarView.setLayoutManager(new GridLayoutManager(mContext,7));

두줄을 추가해주면 해결된다.


블로그 이미지

Link2Me

,
728x90

새로운 Moudle 를 추가했더니 이런 에러가 발생한다.

Manifest merger failed with multiple errors, see logs


보통 다른 블로그에 나온 경우와는 다른 황당한 경우다.


public class Calendar_Item {
    private  String year; // 년
    private  String month; // 월
    private  String day; // 일
    private  int weekday; // 요일
    private  String color; // 색상
    private String event;
    private String key; // 월일 값을 key 로 사용


처음에 이름을 name 으로 했다가 rename 으로 일괄 event 로 변경했더니......

프로젝트내의 모든 AndroidManifest.xml 파일과

res/values/ 하단 파일들이 모두 name → event 로 변경되어 있더라. ㅠㅠ


이런 황당한 경우가 있다니....

그러다보니 AndroidManifest.xml 이 비정상적이다보니 이런 에러메시지를 보여줬던 거다.


모두 수작업으로 EditPlus 에서 event= → name= 로 변경해주고 나서야 정상이 되었다.


변수를 사용할 때 절대로 name 과 같은 걸 사용해서는 안되겠다는 걸 알게 되었다.



블로그 이미지

Link2Me

,
728x90

코드를 구현하다보니 파싱처리를 하는데 동일한 단어가 들어간 곳이 다른 곳에도 존재하는 걸 몰랐다.

파싱할 내용이 다르다보니 에러가 발생했다.


해결법은

try {
    int stlocation = str.indexOf("GW=");
    int endlocation = str.length();
    String temp = str.substring(stlocation + 4, endlocation).trim();
    String[] part = temp.split("\\s{1,}"); // 공백 단위로 메시지를 분리하라.
    if (part[3].length() > 4) {
        ipaddr = part[3];
    }
} catch(IndexOutOfBoundsException e) {
    System.out.println(e);
}


와 같이 try catch 문으로 해결했다.



블로그 이미지

Link2Me

,
728x90

서버가 죽을 수도 있다는 가정하에 코드를 구현했어야 하는데 이를 고려하지 못하니까 어플이 진입도 하지 못하고 죽는 현상이 발생했다.

초보 개발자의 로직 에러다.


이를 보완한 코드 예제다.

서버의 httpd 데몬을 죽여서 전달되는 메시지를 보고, 아래와 같이 failed 라는 글자가 포함되어 있으면, 버전 체크하지 말고 그냥 다음 단계를 실행하도록 했다.

version = Value.getVersionName(mContext);
version = version.replaceAll("[^0-9]", ""); // 버전에서 숫자만 추출

Log.d("WEB", "Response: " + response);
if(response.contains("failed")){ // 서버가 동작하지 않을 때의 메시지에 포함된 걸 체크하라.
    Log.e("WEB", "Server isConnected failed.");
    startActivity(new Intent(getApplication(), MainMenu.class));
    finish();
} else {
    Log.e("WEB", "Server Version : " + response);
    Response = response.replaceAll("[^0-9]", ""); // 버전에서 숫자만 추출
    if (Integer.parseInt(version) < Integer.parseInt(Response)) { // 서버 버전이 더 높으면
        UpgradeProcess();
    } else {
        startActivity(new Intent(getApplication(), MainMenu.class));
        finish();
    }
}


블로그 이미지

Link2Me

,
728x90

FCM 기능을 추가하기 위한 코드 변경을 하다보니 잔뜩 에러가 발생한다.


<provider
  android:name="android.support.v4.content.FileProvider" 에러가 표시하고 컴파일시 에러 메시지를 출력한다.


<provider
    android:name="androidx.core.content.FileProvider"
로 변경해준다.


AndroidX를 사용하려면 컴파일 SDK를 Android 9.0(API 레벨 28) 이상으로 설정하고, gradle.properties 파일에서 다음 두 Android Gradle 플러그인 플래그를 true로 설정해야 한다.

android.useAndroidX: true로 설정하면 Android 플러그인에서 지원 라이브러리 대신 적절한 AndroidX 라이브러리를 사용한다. 지정하지 않으면 플래그는 기본적으로 false이다.
android.enableJetifier: true로 설정하면 Android 플러그인에서 자동으로 기존 타사 라이브러리를 이전하여 바이너리를 다시 작성해 AndroidX를 사용한다. 지정하지 않으면 플래그는 기본적으로 false이다.


참조 : https://developer.android.com/jetpack/androidx?hl=ko

블로그 이미지

Link2Me

,
728x90

LG G5 개발용 폰에서는 테스트하면 정상적으로 잘 설치된다.

그런데 갤럭시노트 8 에서 테스트를 하니까 제대로 설치가 안된다.

"앱이 설치되지 않았습니다." 라면서 설치가 되지 않는다.

 

release 모드로 APK 파일을 생성할 때

Signature Versions : V1 ← OS가 7.0 미만일 때

V2 ← OS가 7.0 이상일 때 체크해서 생성하면 되는데 둘다 체크해서 만들면 된다.

APK 파일이 잘못 생성될 수도 있으므로 프로젝트를 clean 한 후 다시 build한 후 APK를 생성한다.

 

구글은 PHA(유해한 앱) 앱의 비율을 차례대로 줄여 나갔으며, 비공식 마켓 등에서 배포되는 PHA 앱은 여전히 남아 있다.
안드로이드 8.0인 오레오(oreo)부터는 플레이 스토어 를 제외한 비공식 마켓에서 배포되는  앱을 설치할때는 새로운 권한을 얻도록 하였다.

 

1. 이미 동일한 패키지명의 앱이 설치 되어 있는 경우.

   - 이 경우에는 기존 설치된 어플의 흔적을 제거해주고 다시 설치하면 설치가 된다.

 

2. 구글 플레이 프로텍트에서 막은 경우

   - 구글 플레이 스토어 실행

   - 상단 왼쪽에 메뉴 아이콘을 클릭 후 Play 프로텍트 선택

   - "기기에 보안 위협이 있는지 검색" 항목 체크 해제

   - "유해한 앱을 감지하는 기능 보완" 항목 체크 해제

 

안드로이드 8.0 오레오에서 부터 '출처를 알 수 없는 앱' 정책에 변경됐다. 
구글 정책 변경에 따라 기존 방식인 '설정-해제' 방식은 사라지고 각 '앱 별 관리' 방식으로 바뀌게 되었다.

블로그 이미지

Link2Me

,
728x90

그림과 같이 에러 메시지가 나오는 경우가 생긴다.

Unable to resolve dependency




이 메시지가 나오는 원인은 27.+ 라고 되어 있기 때문이다.
+(동적버전번호)를 사용하면 예기치 않는 버전 업데이트를 초래하고 버전 차이점 문제 해결이 어려워질 수 있다.

27.1.1 로 수정하고 나서 에러 발생현상은 없어졌다.


블로그 이미지

Link2Me

,
728x90

컴파일을 시도했더니 이런 메시지가 나온다.

build\intermediates\instant_run_split_apk_resources\debug\instantRunSplitApkResourcesDebug\out\slice_2\resources_ap


해결방법


블로그 이미지

Link2Me

,
728x90

Materrial Design Bootstrap(MDB) 기반으로 코딩한 mobile Web에서 전화걸기 기능을 추가했다.

Android WebView 에서 모바일 Web을 Load하여 전화걸기 클릭을 하니까

err_unknown_url_scheme 에러 메시지를 뿌린다.


 public boolean shouldOverrideUrlLoading(WebView view, String url) {
    view.loadUrl(url);
    return true;
}

로 코드가 되어 있다.


전화걸기 tel:010-XXXX-YYYY 에 대한 예외처리 기능이 없어서 에러가 발생한 것이다.


아래와 같이 코드를 보완하고 나서 테스트하니 전화걸기가 잘 된다.

@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
    if (url.startsWith("tel:")) {
        Intent call_phone = new Intent(Intent.ACTION_CALL);
        call_phone.setData(Uri.parse(url));
        startActivity(call_phone); // 권한 설정은 Loing.java에서 처리했음
    } else if (url.startsWith("sms:")) {
        Intent intent = new Intent(Intent.ACTION_SENDTO, Uri.parse(url));
        startActivity(intent);
    } else if (url.startsWith("intent:")) {
        try {
            Intent intent = Intent.parseUri(url, Intent.URI_INTENT_SCHEME);
            Intent existPackage = getPackageManager().getLaunchIntentForPackage(intent.getPackage());
            if (existPackage != null) {
                startActivity(intent);
            } else {
                Intent marketIntent = new Intent(Intent.ACTION_VIEW);
                marketIntent.setData(Uri.parse("market://details?id=" + intent.getPackage()));
                startActivity(marketIntent);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
        }
    } else {
        view.loadUrl(url);
    }
    return true;
}





블로그 이미지

Link2Me

,
728x90

Error: Program type already present: android.support.v4.app.BackStackRecord$Op


컴파일 하니까 에러가 발생하면서 컴파일이 안된다.


implementation fileTree(dir: 'libs', include: ['*.jar'])

하고 중복이 발생하는 부분이 있나 싶어서


dependencies {
    //implementation fileTree(include: ['*.jar'], dir: 'libs')
    implementation 'com.android.support:appcompat-v7:28.0.0'
    implementation 'com.android.support:support-v4:28.0.0'
    implementation 'com.android.support.constraint:constraint-layout:1.1.3'
    implementation files('libs/d2xx.jar')
}

로 하고 컴파일을 하니까 에러가 발생하지 않고 잘 된다.





블로그 이미지

Link2Me

,
728x90

Android Data 를 가져오기를 하는데 파일 복호화 시 IllegalBlockSizeException 발생한다.


원인은 Copy & Paste 를 하면서 파일 암호화를 하지 않았는데, 암호화한줄 알고 복호화를 시도해서 발생했다.

암호화, 복호화가 정확하게 맞는 것인지 체크를 하지 못한 문제였다.


암호화, 복호화 함수는 잘 만들어져 있는데도 불구하고 다른 암호화 모듈로 변경도 시도해보고 했다.

그런데, 암호화/복호화가 문제없는 코드도 있었으므로 암호화/복호화 코드 문제는 아니라고 판단을 했어야 하는데, 문제는 PHP 코드에서 암호화를 하는 부분에서 암호화하지 않는 부분을 Android 에서 복호화를 시도하고 있었던 것이었다.

아무튼 기본기가 약하면 손발이 고생하는 수 밖에 없다.



블로그 이미지

Link2Me

,
728x90

XMPP Chat 기능 동작 테스트를 하려고 GitHub 자료를 다운로드 받아서 테스트 해보고 있다.

벌써 여러차례 로그인 실패를 겪으면서 GitHub 에 있는 자료와 동일한 환경으로 설정을 해보고자 프로젝트를 별도로 추가하는 방식으로 테스트하고 있다.


dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'com.android.support:appcompat-v7:23.2.1'
    implementation 'com.android.support.constraint:constraint-layout:1.1.3'
    implementation 'com.android.support:design:23.2.1'
    implementation 'com.android.support:recyclerview-v7:23.2.1'
    implementation 'org.igniterealtime.smack:smack-android:4.1.1'
    implementation 'org.igniterealtime.smack:smack-tcp:4.1.1'
    implementation 'org.igniterealtime.smack:smack-core:4.1.1'
    implementation 'org.igniterealtime.smack:smack-im:4.1.1'
    implementation 'org.igniterealtime.smack:smack-extensions:4.1.1'
    implementation 'org.igniterealtime.smack:smack-android-extensions:4.1.1'
    implementation 'org.igniterealtime.smack:smack-resolver-minidns:4.1.3'
    implementation 'org.igniterealtime.smack:smack-sasl-provided:4.1.1'
    implementation 'com.google.code.gson:gson:1.7.2'
    implementation 'com.google.android.gms:play-services:8.1.0'
}


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

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


AndroidManifest.xml 에서 에러가 발생하면 위와 같이 하면 해결되더라.


그런데 문제는
implementation 'com.google.android.gms:play-services:8.1.0' 때문에 multidex 에러가 발생하는지 오류가 또다시 발생한다.

이건 시도해보려고 했지만 성공하지 못했다.



블로그 이미지

Link2Me

,
728x90

Android 채팅창 구현을 해보려고 시도하면서 SoftKeyboard 가 활성화되면 Recyclerview 입력한 내용이 자동으로 스크롤 되는 기능이 제공되어야 할 거 같아서 EditText 터치이벤트 등으로 시도를 해보왔으나 실패!!!


http://givenjazz.tistory.com/54 에 나온 코드가 내가 원하는 코드였다.

이 코드 덕분에 코드가 좀 더 정교해지고 있는거 같다.


import android.app.Activity;
import android.content.Context;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.view.View;

public class SoftKeyboardDectectorView extends View {
    // 출처 : http://givenjazz.tistory.com/54
    private boolean mShownKeyboard;
    private OnShownKeyboardListener mOnShownSoftKeyboard;
    private OnHiddenKeyboardListener onHiddenSoftKeyboard;

    public SoftKeyboardDectectorView(Context context) {
        this(context, null);
    }

    public SoftKeyboardDectectorView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        Activity activity = (Activity)getContext();
        Rect rect = new Rect();
        activity.getWindow().getDecorView().getWindowVisibleDisplayFrame(rect);
        int statusBarHeight = rect.top;
        int screenHeight = activity.getWindowManager().getDefaultDisplay().getHeight();
        int diffHeight = (screenHeight - statusBarHeight) - h;
        if (diffHeight > 100 && !mShownKeyboard) { // 모든 키보드는 100px보다 크다고 가정
            mShownKeyboard = true;
            onShownSoftKeyboard();
        } else if (diffHeight < 100 && mShownKeyboard) {
            mShownKeyboard = false;
            onHiddenSoftKeyboard();
        }
        super.onSizeChanged(w, h, oldw, oldh);
    }

    public void onHiddenSoftKeyboard() {
        if (onHiddenSoftKeyboard != null)
            onHiddenSoftKeyboard.onHiddenSoftKeyboard();
    }

    public void onShownSoftKeyboard() {
        if (mOnShownSoftKeyboard != null)
            mOnShownSoftKeyboard.onShowSoftKeyboard();
    }

    public void setOnShownKeyboard(OnShownKeyboardListener listener) {
        mOnShownSoftKeyboard = listener;
    }

    public void setOnHiddenKeyboard(OnHiddenKeyboardListener listener) {
        onHiddenSoftKeyboard = listener;
    }

    public interface OnShownKeyboardListener {
        public void onShowSoftKeyboard();
    }

    public interface OnHiddenKeyboardListener {
        public void onHiddenSoftKeyboard();
    }
}
 


사용법

public class ChatBoxActivity extends AppCompatActivity {
    Context context;

    public RecyclerView listView;
    public ArrayList<Chat_Item> chatItemList = new ArrayList<>(); ;
    RecyclerView.Adapter listViewAdapter;
    RecyclerView.LayoutManager layoutManager;

    public  EditText editText;
    public  Button send ;
    //declare socket object
    private Socket socket;
    public String Nickname;


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

        editText = (EditText) findViewById(R.id.message) ;
        send = (Button)findViewById(R.id.send);
        // get the nickame of the user
        SharedPreferences pref = getSharedPreferences("pref", Activity.MODE_PRIVATE);
        String userNM = pref.getString("userNM", "");
        String userID = pref.getString("userID", "");
        Nickname = userNM +"("+userID+")";

        final SoftKeyboardDectectorView softKeyboardDecector = new SoftKeyboardDectectorView(this);
        addContentView(softKeyboardDecector, new FrameLayout.LayoutParams(-1, -1));

        softKeyboardDecector.setOnShownKeyboard(new SoftKeyboardDectectorView.OnShownKeyboardListener() {
            @Override
            public void onShowSoftKeyboard() {
                //키보드 등장할 때 채팅창 마지막 입력 내용을 바로 보이도록 처리
                listView.scrollToPosition(chatItemList.size()-1);
            }
        });

        try {
            socket = IO.socket(Value.ChatLive); // 채팅서버 주소
            socket.connect();
            socket.emit("join", Nickname);
        } catch (URISyntaxException e) {
            e.printStackTrace();
        }


       // Adapter에 추가 데이터를 저장하기 위한 ArrayList
        listView = (RecyclerView) findViewById(R.id.messagelist);
        RecyclerView.LayoutManager layoutManager = new LinearLayoutManager(getApplicationContext());
        listView.setLayoutManager(layoutManager);
        listView.setItemAnimator(new DefaultItemAnimator());

        listViewAdapter = new ListViewAdapter(chatItemList, this); // Adapter 생성
        listView.setAdapter(listViewAdapter); // 어댑터를 리스트뷰에 세팅

    }
}
 


채팅 내용을 마지막으로 입력한 내용이 소트프키보드 입력창 위에 바로 보이도록 하는 코드다.

블로그 이미지

Link2Me

,
728x90
Android 채팅 라이브러리를 추가하면 에러가 발생한다.

implementation('com.github.nkzawa:socket.io-client:0.5.0') {
    exclude group: 'org.json', module: 'json'
}

원인을 알고보니 targetSdkVersion 26 에서는 com.github.nkzawa:socket.io-client:0.3.0 으로 하면 정상 동작한다.
com.github.nkzawa:socket.io-client:0.5.0 은 targetSdkVersion 27 에서 정상 동작하더라.

Creating a realtime chat app with android , NodeJs and Socket.io 에 대한 최신 예제는 https://dev.to/medaymentn/creating-a-realtime-chat-app-with-android--nodejs-and-socketio-4o55 를 참조하여 실행하면 Node.js 와 안드로이드간 통신이 간단하게 된다는 걸 확인할 수 있다.

회원정보와 연동하고, 내가 보낸 메시지, 상대방이 보낸 메시지를 구분하는 것은 안된다.

회원정보 연동은 Android-PHP-MySQL 통신으로 획득한 정보를 Node.js-Android 간에 활용하면 된다.
내가 보낸 메시지와 상대방이 보낸 메시지 구분은 RecyclerView 기능을 활용하면 해결 할 수 있다.


블로그 이미지

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

,