Android2023. 5. 8. 11:22

내가 카메라로 촬영한 사진이나 영상을 다른 앱으로 보고자 하는 경우 이전에는 아래와 같이 하였다.

 

File file = new File('파일경로');
Intent. inetnt = new Intent();
intent.setAction(Intent.ACTION_VIEW);
intent.setDataAndType(Uri.fromFile(file), "image/png");
startActivity(intent);

하지만 안드로이드 10부터 Scopped storate모드가 지원되면서 안되기 시작하더니 이제는 그냥 안된되고 보면 될것 같다.

이젠 아래와 같이 호출해야 정상적으로 다른앱으로 내가 만든 사진을 볼수가 있다. 그것도 앱이 사용중에만.

File file = new File('파일경로');
Intent intent = new Intent();
intent.setAction(Intent.ACTION_VIEW);
intent.sestDataAndType(
	FileProvider.getUriForFile(this, getPackageName()+".fileprovider", file),
    "image/png");
intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
startActivity(intent);

Uri.fromFile()이 아니라 FileProvider.getUriForFile(...)를 사용하였고 FLAG_GRANT_READ_URI_PERMISSION권한을 추가해 주었다. 이 권한은 호출한 앱의 테스크가 유지되는 동안 유지된다. 즉 앱이 종료될때까지 된다고 보면 될것 같다.

 

'파일경로'라고 되어 있는 부분은 아무 경로나 되는것이 아니며 일반적으로 개발자들은 sdcard에 저장하길 원할것이다.

앱패키지명이 'com.my.app'이라면 '/sdcard/Android/data/com.my.app/files'가 된다.

FileProvider에 대한 글은 많이 있기 때문에 자세한 설명은 하지 않고 만일 /image폴더에 저장하고 그 파일을 다른 앱으로 보고자 할 경우에는 아래와 같이 하면 된다.

 

AndroidManifest.xml

    <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" />
    </provider>

/res/xml/provider_paths.xml

<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <external-files-path name="external_files" path="." />
</paths>

/sdcard/Android/data/com.my.app/files/image 폴더내의 sample.png파일은 아래와 같이 지정한다.

File file = new File(getExternalFilesDir("image") + "/sample.png");

이렇게 지정된 파일을 FilrProvider.getUriForFile()로 Uri를 얻어내어 Intent에 던져 주면 이미지파일을 열 수 있는 다른 앱이 이미지를 보여줄수 있게 된다.

 

provider_paths.xml에서 외부에 공유 가능한 폴더 목록을 지정하게 되어 있는데 external-files-path이외에 아래와 같이 추가로 지정할 수 있다.

 

files-path Context.getFilesDir()에 해당
cache-path Context.getCacheDir()에 해당
external-path Environment.getExternalStorageDirectory()에 해당
external-cache-path Context.getExternalCacheDir()에 해당
external-media-path Context.getExternalMediaDirs()에 해당

 

Posted by 삼스
카테고리 없음2023. 3. 14. 14:05

Gitlab으로 CI/CD가 가능한데 기본적인 사항을 정리해 보겠다.

 

Gitlab에 저장소를 만들고 .gitlab-ci.yml파일을 만들면 그 때부터 시작된다.

샘플은 https://gitlab.com/nanuchi/gitlab-cicd-crash-course 로 하겠다.

 

샘플프로젝트는 python으로 작성된 시스템모니터링정보를 제공하는 웹서비스이다.

.gitlab-ci.yml파일

variables:
  IMAGE_NAME: nanajanashia/demo-app
  IMAGE_TAG: python-app-1.0

stages:
  - test
  - build
  - deploy

run_tests:
  stage: test
  image: python:3.9-slim-buster
  before_script:
    - apt-get update && apt-get install make
  script:
    - make test


build_image:
  stage: build
  image: docker:20.10.16
  services:
    - docker:20.10.16-dind
  variables:
    DOCKER_TLS_CERTDIR: "/certs"
  before_script:
    - docker login -u $REGISTRY_USER -p $REGISTRY_PASS
  script:
    - docker build -t $IMAGE_NAME:$IMAGE_TAG .
    - docker push $IMAGE_NAME:$IMAGE_TAG


deploy:
  stage: deploy
  before_script:
    - chmod 400 $SSH_KEY
  script:
    - ssh -o StrictHostKeyChecking=no -i $SSH_KEY root@161.35.223.117 "
        docker login -u $REGISTRY_USER -p $REGISTRY_PASS &&
        docker ps -aq | xargs docker stop | xargs docker rm &&
        docker run -d -p 5000:5000 $IMAGE_NAME:$IMAGE_TAG"

 

variables는 스크립트내에서 사용되는 변수를 정의할 수 있다.

variables:
  IMAGE_NAME: nanajanashia/demo-app
  IMAGE_TAG: python-app-1.0

stage는 각 작업의 단계를 나타내며 순차적으로 수행이된다. 작업은 병렬로 처리되는데 같은 stage의 작업들 끼리만 병렬로 처리될 수 있으며 중간에 에러 발생 시 다음단계로 넘어가지 않는다. 샘플에서는 3개의 stage가 있으며 각각 run_tests, build_image, deploy 작업이 한개씩만 있다.

 

stages: // 스테이지 정의
  - test
  - build

run_tests:
  stage: test // run_tests는 test 스테이지
  ...

build_image:
  stage: build // build_image는 build 스테이지
  ...

run_tests 작업은 test stage이고 python docker image기반에서 make가 설치되어 있어야 하고 make test를 실행하여 테스트를 진행한다.

run_tests:
  stage: test
  image: python:3.9-slim-buster
  before_script:
    - apt-get update && apt-get install make
  script:
    - make test

build_image 작업은 도커이미지를 만들어서 Docker hub에 도커이미지를 push하는 작업까지 진행한다.

deploy 작업은 서버에 접속하여 docker image를 적용하고 서비스를 시작한다.

서버에 접속시 ssh로 접속하기 때문에 인증을 위해 key를 등록해주어야 한다.

 

gitlab에서는 개인키를 설정 -> CI/CD -> Variables에서 추가해 줄 수 있으며 위 예에서는 SSH_KEY로 등록된 예이다. SSH_KEY의 경우 파일이며 이때는 type을 File로 등록하면 된다.

Variables에 다른 Docker 로그인 계정과 비번도 등록해서 사용하고 있다. REGISTRY_USER, REGISTRY_PASS가 이 샘플의 예이다. 계정이나 비번을 스크립트에 노출시키지 않기 위해서는 이 방법을 추천한다.

Posted by 삼스
카테고리 없음2023. 2. 8. 15:57

chatGPT가 하도 핫 하길래 오늘 좀 놀아봤다.

개발자의 호기심으로 코딩을 좀 물어 봤는데 깜놀~

 

먼저 문제가 있는 코드를 주고 물어 봤다.

완전 놀랐는데..

놀란 이유는 오류를 찾았을 뿐 아리나 내가 memcpy의 src와 dest위치를 일부러 바꿔서 예시를 주었는데 그 부분도 수정이 되었다.

memcpy(src, dest)가 memcpy(dest, src, size)로 바뀐것이다.

이놈이 코드의 맥락까지 파악하는건가? 하고 놀래서 좀더 물어봤다.

 

swift code로도 훌룡하게 바꿔주었다.

한번더 물어보았다.

 

이건 좀 이상했다. dest를 src로 복사하고 있네...

그래서 내가 좀 지적했다.

 

코드는 맞게 수정되었는데 "아래 코드는 대상버퍼에서 원본버퍼로 복사하는것입니다."라고 대답했다. 대답이 틀렸넹.

쪼오금 부족했지만 참 대단한 넘이다.

간단한 코드 스니펫을 작성하거나 점검할때만 사용해도 코딩시간을 줄여줄 수 있지 않을까 싶다.

이게 더 똑똑 해지면 내 일이 없어지는건 아닐지.. 생각해 보게 되는데 이 부분에 대한 인사이트가 있는지 찾아봐야 겠다.

 

Posted by 삼스
카테고리 없음2022. 12. 8. 13:50

먼가 힙한 언어가 나왔다. 만들기 시작한지는 20년가까이 되었지만 실제 사용이 되기 시작한지는 5년여가 된듯 하다.

 

이걸 또 알아바야 겠다.

 

마스코드가 꽃게모양인데 rust사용자를 Rustancean이라고 부른다.

 

영문공식문서 : https://doc.rust-lang.org/book/title-page.html

한글공식문서 : https://rinthel.github.io/rust-lang-book-ko/foreword.html

현행화가 한글이 부족하다고 한다. 영문이 먼저 업데이트 되고 한글이 업데이트 될텐데 잘 안되나 보다.

위 문서가 러스트 프로그래밍 공식 가이드 라는 서적으로 출간되어 있는데 오역도 많아서 안보는게 낳다고 한다.

 

신생언어인만큼 많은 변경이 계속 있어왔고 앞으로도 변경이 있을 수 있기 때문에 안바뀔것 같은것 위주로 알아보고자 한다.

 

Graydon Hoare(그레이던 호어)라는 모질라 개발자가 2006년에 개인 프로젝트로 시작되었다고 한다. 이 후 모질라가 공식적으로 후원을 하여 2015년에 비로소 1.0이 세상에 나왔다고 한다. 참 대단한게 개인이 이런걸 시작하고 또 회사에서 여기에 투자를 할 수 있는 문화가 너무 부럽다. 회사나 개발자나 참 프로답다는 생각이 든다.. 그에 비해 나는.. ㅎㅎㅎ;;;

 

이 후 많은 인기를 누렸고 여러 서비스들에서 사용이 되었다. discord의 서버의 상당부분이 rust로 되어 있다고 한다.

 

주요한 특징은 다음과 같다.

 

  • 안정성을 추구한다.
  • 병렬성을 추구한다.
  • 현대적이다(Modern language)

 

GC?

 

컴파일언어로서 특이한 특징을 갖는다.

GC를 살펴보자면 다른 언어들의 GC는 GC collector가 메모리를 계속 감시하면서 조건이 맞게 되면 메모리를 해제하는 방식으로 동작한다.

Rust는 컴파일타임에 메모리를 정리하는 코드를 삽입한다. C언어는 직접 메모리를 할당하고 직접 해제해야 하는데 이런 오류를 줄여주기 위해 컴파일러가 직접 해당 코드를 삽입한다는 것이다. 무슨 AI가 코딩하는것 같다. GC는 언제 일어날지 모르는데 반해 Rust는 컴파일타임에 이미 정해져 있다. 어쨌든 이런 방법으로 Null Pointer에러는 안녕이다.

 

소유권

 

여기에 소유권이라는 독특한 개념이 추가 되었다. 이는 모든 값에 ownership이라는 변수가 있다. 모든 값은 하나의 소유자만 있을 수 있고 소유자가 범위를 벗어나면 값이 삭제되면서 메모리에서 해제가 된다.

 

 

시작하기

 

Rust toolchain installer 다운로드 및 설치

$ curl —proto ‘=https’ —tlsv1.2 -sSf https://sh.rustup.rs | sh

 

rustc(컴파일러), cargo(패키지매니저)가 설치된다. 

Cargo는 빌드, 테스트, 문서화, 배포등 개발관련 모든 과정을 커버한다.

 

cargo new : 새 프로젝트를 생성하고 Cargo.toml에 디펜던시를 추가

cargo build : 프로젝트 빌드

cargo run : 컴파일및 실행

 

rustup으로 툴체인에 다양한 유틸리티를 추가해줄수 있다.

rustfmt: 코딩 스타일가이드에 맞춰주는 포매터

clippy: 코드상의 실수와 개선점을 제안

 

$ rustup update

$ rustup component add rustfmt clippy rls rust-analysis rust-src

 

fn main() {

  let x: i8 = 10;  // let으로 변수 선언

  let mut y: i8 = 20; // mut로 변경가능한 변수 선언

  y = y + 20;

  let z: i32 = 100; // i8은 8비트 정수, i32는 32비트 정수, f32: 부동소수, bool: 불리언타입

  println!{“{}”, x+y);

}

 

fn square(x: i32) -> i32 {

  x * x // 마지막 표현식이 리턴값이 된다. ;(세미콜론)을 넣지 않는다

}

 

let x = 1;

let y = { // 변수 선언에도 구문식 사용이 가능

  let x = 2;

  x +1

};

 

let y = if x == 1 {

  10

} else if x > 1 {

  20

} else {

  30

};

 

타언어의 switch같은 match키워드가 있다.

 

match x {

1 => println!(“one”),

2 => println!(“two”),

3 => println!(“three”),

_ => (),

}

 

 

Nullable

 

swift나 kotlin의 optional에 해당하는 Option<T>열거형이 제공된다.

 

enum Option<T> {

  Some(T),

  None,

}

 

null일 수 있는 변수는 Option<T> 타입으로 사용할 수 있다.

 

fn plus_one(x: Option<i32>) -> Option<i32> {

  match x {

    None => None,

    Some(i) => Some(i+1),

  }

}

 

let one: Option<i32> = Some(1);

let two = plus_one(one);

let three = plus_one(two);

 

너무나도 명시적으로 이놈은 null일수 있다는것을 알수 있게 설계되었다는것을 알 수 있다. swift나 kotlin이 rust의 Option을 따라한것 같은 느낌이 강하게 드는 부분이다.

 

 

오너쉽(Ownership)

 

메모리를 관리하기 위한 방법으로 GC에 비해 참 맘에 드는 부분이다. 

모든 값은 owner라는 변수를 갖고 한번에 하나의 오너만 가질수 있고 범위를 벗어나면 해제된다.

 

let x = 3;

let y = x;

 

x, y는 모두 프리미티브타입의 값으로 스택메모리에서 운영되며 x에 3이 바인딩되고 y에 그 값이 복사된다. x, y 모두 3이된다.

 

let s1 = String::from(“say hello”)

let s2 = s1

 

String::from은 let s = “hello”처럼 문자열 리터럴을 할당하지 않고 힙에 할당하게 된다. 따라서 런ㄹ타임에 문자열을 수정할 수 있다.

s2 = s1에서 s1을 s2에 할당했다. 이는 오너쉽이 변경됨을 의미한다. s1의 정보는 스택에 문자열이 할당된 포인터(ptr), 문자열길이(len), 용량(capacity)정보가 담기게 되는데 s2에 할당하는 순간 s2의 정보가 스택에 새로 생성되고 ptr, len, capacity도 복사가 된다. 이 때 s1은 무효화한다. 따라서 s2의 ptr만 실제 문자열의 포인터를 가리키게 된다. 이제 s2가 범위를 벗어날때 s2의 drop메소드가 호출되고 포인터도 해제되어 메모리가 수거되게 된다.

이는 함수의 인자로 넘어갈때도 마찬가지이며 함수의 반환값으로 넘길때도 마찬가지이다.

 

let s = String.from(“say hello”);

print_hello(s);

// 여기서 s는 유효하지 않기 때문에 사용할 수 없다. 

 

함수의 인자로 넘기되 오너십을 이동시키고 싶지 않을 때 는 참조만 넘겨준다. 이를 Borrowing(빌림)이라고 한다.

 

let s = “say hello”;

let len = get_length(&s);

 

fn get_length(s: &String) -> usize {

  s.len()

}

 

오너쉽을 넘기지 않기 때문에 s가 빌림 인자로 넘어갈때 스택에는 포인터만 저장되고 문자열의 길이와 용량정보는 저장되지 않는다. 그러면 함수내에서 참조가 가능하다. 

이 때 참조만 하지 않고 빌린값을 변경하고자 한다면 mut 를 사용해야 한다.

 

let mut hello = String::from(“hello”);

change(&mut hello);

println!(“{}”, hello);

 

fn change(s: &mut String) {

  s.push_str(“, world”);

}

 

change에서 문자열의 값을 변경 하였는데 실제로 힙에서 할당한 주소공간에서 마음대로 용량을 늘릴수 없기 때문에 새로운 메모리공간을 확보하여 처리하게 된다. 따라서 ptr값이 변경이 된다.

Rust는 위와 같이 아주 작은 양의 값을 할당할때도 메모리 재할당을 하지 않도록(성능저하를 발생시킴) 하기 위해 애초에 용량을 좀더 크게 잡아준다. “hello” 다섯 글자의 경우에도 넉넉히 30자 정도의 메모리 공간을 할당한다.

 

가변 참조를 빌려줄때 주의 할점은 한스코프안에서 한번만 가능하다는 것이다.

 

let r1 = &mut s;

let r2 = &mut s;

 

s의 가변참조를 r1과 r2에 할당했는데 이 때 먼저 수행한 r1의 가변참조는 무효화된다. 만일 이런식으로 코딩하면 컴파일 단계에서 걸러준다.

 

error[E0499]: cannot borrow `s` as mutable more than once at a time

 

 

Panic!

 

언어에 패닉이라는 키워드가 사용되다니.. 참 재미있게 느껴진다. 패닉의 뜻에서 느껴지듯이 굉장히 큰 문제가 발생했을때 임을 알 수 있다.

panic!(“메세지”)은 메세지를 출력하고 앱을 종료시킨다.

 

러스트는 또 Result 열거형을 제공한다.

 

enum Result<T, E> {

  Ok(T),

  Err(E),

}

 

어떤 함수가 Result를 반환하면 match로 분기처리할 수 있다.

 

let file = FIle::open(“some file”);

let file = match file {

  Ok(f) => f,

  Err(error) => panic!(“Failed to open the file: {:?}”, error)

};

 

여기까지 몇가지 특성을 알아 보았는데..

더 자세히는 공식문서를 보면서 알아보아야 할것이다.

 

Rust는 타깃이 성능향상에 맞춰져 있으며 따라서 고성능을 요하는 서버 시스템프로그램에 적당해 보인다.

 

안드로이드도 플랫폼에 Rust를 적용해 나가고 있다고 한다.

https://source.android.com/docs/setup/build/rust/building-rust-modules/overview?hl=ko

Posted by 삼스
iOS2022. 12. 8. 13:37

 

애플에서 더이상 엔터프라이즈 계정을 만들어 주지 않으려고 한다고 한다.

새로 만들려고 하면 까다로운 조건을 걸어서 포기하게 하고 기존 계정같은 경우도 연장을 잘 안해주려고 한다고 한다.

그에 대한으로 스토어계정으로 등록되지 않은 앱 배포 라는 방식을 제안한다고 한다.

이렇게 등록된 앱은 스토어에 게시되지 않고 검색해도 나오지 않으며 별도로 제공된 링크를 통해서만 설치가 가능하다.

엔터프라이즈 계정은 아무런 제약없이 단말의 모든 기능을 무제한으로 사용할 수 있고 심사를 거치지 않기 때문에 애플이 허용하지 않는 목적의 앱들도 배포가 가능하기 때문에 많은 오남용 사례가 있었다고 한다.

하지만 갑자기 엔터프라이즈 계정을 모두 막을수는 없기 때문에 등록되지 않은 앱이라는 단계를 추가하여 내부적으로 링크를 통해서만 배포할 수 있는 방법을 제공하기 시작한것이다.

 

등록되지 않은 앱을 등록하면 스토어앱보다 느슨하게 심사가 이루어진다고 하며 실제로 스토어앱보다 빠르게 심사가 완료되었다.

 

등록되지 않은 앱으로 등록하려면 스토어앱 등록절차대로 진행하여 심사에 제출까지 되어 있어야 한다. 이 상태에서 제출물의 심사 메모 섹션에 등록되지 않은 앱으로 배포해야 하는 이유를 명시하고 앱을 제출한다.

 

자세한 정보는 아래 링크 참조

 https://developer.apple.com/kr/support/unlisted-app-distribution/ 

Posted by 삼스
Flutter2022. 8. 2. 16:27

Dart 언어 둘러보기

 

자세히 알아볼 시간이 없다.. 

다른 언어 공부할때 처럼 휘리릭 둘러보자. 자세한건 코딩하면서 …

 

c++ 비슷하면서 kotlin같기도 하고 머 그렇다.

 

void main() {
  print(‘Hello, World!’);

}

타입추론이 적용되고 타입체크가 강력하게 이루어진다.

 

var name = ‘Voyager I’; // 문자열

var year = 1977; // 숫자

var antennaDiameter = 3.7; // 부동소수

var flybyObjects = [‘Jupiter’, ‘Saturn’, ‘Uranus’, ‘Neptune’]; // 배열

var image = { // 딕셔너리

  ‘tags’: [‘stern’],

  ‘url’: ‘//path/to/saturn.jpg’

};

흐름제어

 

 if (year >= 2001) {
   print(‘21st century’);
 } else if (year >= 1901) {
   print(‘20st century’);
 }

for (final object in flybyObjects) {
  print(object)
}

for  (int month=1; month<=12; month++) {
  print(month);
}

While (year < 2016) {
  year += 1;
}

 

함수

 

int fibonacci(int n) {
  if (n=0 || n==1) return n;
  return fibonacci(n - 1) + fibonacci(n - 2);
}

var result = fibonacci(20);

스트림함수호출을 아래와 같이 할 수 있으며 함수를 인자로 사용할수도 있다. 아래 코드에서 forEach의 print가 함수 인자로 사용된것을 볼수 있다.

 

flybyObjects.where((name) => name.contains(‘turn’)).forEach(print);

 

import

 

// Dart core lib 포함
import ‘daar:math’;

// 외부 패키지 라이브러리 포함
Import ‘package:test/test.dart’;

// 다른 파일 포함
Import ‘path/to/my_other_file.dart’;

클래스

 

class Spacecraft {
  String name;
  DateTime? launchDate;  // nullable
  int? get launchYear => launchDate?.year;   // getter
  Spacecraft(this.name, this.launchDate) {. // 생성자.
  }
  Spacecraft.unlaunched(String name) : this(name, null); // 이름을 갖는 생성자.

  // 메서드
  void describe() {
    print(‘Spacecraft: $name’);
    var launchDate = this.launchDate;
    if (launchDate != null) {
      int years = DateTime.now().difference(launchDate).inDays ~/ 365;
      print(‘Launched: $launchYear ($years years ago)’);
    } else {
      print(‘Unlaunched’);
    }
  }
}

var voyager = Spacecraft(‘Voyager I’, DateTime(1977,  9, 5));
voyager.describe();

var voyager3 = Spacecrafe.unlaunched(‘Voyager III’);
voyager3.describe();

 

열거자(Enums)

 

// 일반 열거자
enum PlanetType { terrestrial, gas, ice }

// 고급열거자
enum Planet {
  mercury(planetType: PlanetType.terrestrial, moons: 0, hasRings: false),
  venus(planetType: PlanetType.terrestrial, moons: 0, hasRings: false),
  // … 
  uranus(planetType: PlanetType.ice, moons, 27, hasRings: true),
  neptune(planetType: PlanetType.ice, moons: 14, hasRings: true;
  //  constant 생성자
  const Planet ( { required this.planetType, required this.moons, required this.hasRings } );

  final PlanetType planetType;
  final int moons;
  final bool hasRings;
}

usage

final yourPlanet = Planet.earth;
if (!yourPlanet.hasRings) {
  print(‘Your planet doesn’t has Rings’);
}

 

클래스 상속

 

class Orbiter extends Spacecraft {
  double altitude;
  Orbiter(super.name, DateTime super.launchDate, this.altitude);
}

Mixins

 

다중클래스 구조에서 코드를 재사용하는 방법을 제공한다. C++의 다중상속이 복잡함때문에 다른 언어는 다 삭제 했는데 얘는 왜 있는건지 모르겠다.

 

mixin Piloted {
  int astronauts = 1;
  void describeCrew() {
    print(‘Number of astronauts: $astronauts’);
  }
}
class PilotedCraft extends Spacecraft with Piloted {
  …
}

PilotedCraft가 astronauts필드를 갖게 되었고 describeCrew()호출이 가능해졌다.

 

interface

 

dart는 interface 키워드가 없으며 모든 클래스는 기본적으로 인터페이스로 간주해도 된다.

 

class MockSpaceCraft implements Spacecraft {
  // …
}

mixin과 implements와 머가 다를까?

일단 implements만으로도 클래스의 다중상속이 가능해진다.

extends로는 다중상속이 안된다. implements와 mixin을 혼용하니 컴파일오류가 난다.

흠 다중상속을 오랫동안 잊고 살았는데 다트에서 부활하니 참 당황스럽네..

왜 이런걸 이런식으로 지원하는지 언어 설계자의 머리속에 들어가보고 싶다.

mixin설계자와 implements설계자가 다른걸까?

 

abstract 클래스는 그리 헤깔리지 않게 지원한다.

 

abstract class Describable {
  void describe();
  void describeWithEmphasis() {
    print(‘===========‘);
    describe();
    print(‘===========‘);
  }
}

비동기호출

 

Future를 제공하여 비동기 호출을 지원한다.

 

const oneSecond = Duration(seconds: 1);

Future<void> printWithDelay(String message) async {
  await Future.delayed(oneSecond);
  print(message);
}

위 코드는 다음코드와 동일하다.

 

Future<void> printWithDelay(String message) {
  return Future.delayed(oneSecond).then((_) {
    print(message);
  });
}

아래 코드를 보면 async, await의 사용이 코드를 얼마나 읽기 쉽게 해주는지 확인할 수 있다.

 

Future<void> createDescriptions(Iterable<String> objects) async {
  for (final object in objects) {
    try {
      var file = File(‘$object.txt’);
      if (await file.exists()) {
        var modified = await file.lastModified();
        print(‘FIle for $object aleady exists. It was modified on $modified.’);
        continue;
      } 
      await file.create();
      await file.writeAsString(‘Start describing $object in this file.’);
    } on IOException catch (e) {
      print(‘Cannot create description for $object : %e’);
    }
  }
}

async*를 사용하여 더 나이스한 코딩이 가능하다.

 

Stream<String> report(Spacecraft craft, Iterable<String> objects) async* {
  for (final object in objects) {
    await Future.delayed(oneSecond);
    yield ‘${craft.name} flies by $object’);
  }
}

예외

 

예외가 필요하면 예외를 아래와 같이 발생시킨다.

 

if (astronauts == 0) {
  throw StateError(‘No astronauts.’);
}

try {
  for (final object in flybyObjects) {
    var description = await File($object.txt).readAsString();
    print(description);
  }
} on IOException catch (e) {
  print(‘Could not describe object: $e’);
} finally {
  flybyObjects.clear();
}

 

함수 파라메터 

 

함수를 인자로 넘길 수 있으며 이를 통해 콜백함수를 사용할 수 있다.

 

void getResultWithCallback(void onResult(String msg)) async {
  await Duration(seconds: 5);
  onResult('on result!!!');
}

getResultWithCallback((ret)=> {
  print(ret)
});

 

 

아주 아주 퀵하게 훓어 보았다.

 

Posted by 삼스
Android2022. 8. 2. 11:49

최근 구글 등록중에 아래와 같은 오류 메시지를 받았다.

 

잠시 당황??? 하고 .. 알아보았는데..

 

AES암호화 시 키유출에 대한 안드로이드의 잠재적 위험성이 이미 유명해져서 더이상 방관하지 않겠다는 구글의 의지를 확인하게 되었다.

 

해킹기술도 많이 알려져서 쉽게 가능한것도 문제의 원인인데 

  1. 안드로이드 디컴파일을 통한 코드 확인 가능 : 해커들은 난독화를 거쳤더라도 어떻게는 패턴을 찾아 키를 알아내려고하고 생각보다 쉽다.
  2. jni로 작성하더라도 objdump를 통해 .data, .text섹션을 뒤지다 보면 발견도 가능
  3. SharedPreference 로 작성된 xml파일을 내부저장소에 저장 하더라도 추출 가능

 

위 방법으로 또 알아낼수도 있다. ECB가 아니라 CBC알고리즘을 사용하여 IV가 추가로 필요하더라도 IV도 동일한 방법으로 유추가 가능.

 

이러저러한 이유로 아뭏든 못 믿겠다고 구글은 판단하고 KeyStore를 사용하란다.

 

이제 KeyStore에 대해 알아보겠다.

 

문서(https://developer.android.com/training/articles/keystore)에 따르면 “Android KeyStore”시스템을 사용하면 암호화키를 컨테이너안에 저장하여 기기에서 키를 추출하기 어렵게 할수 있고, 키저장소에 키가 저장되면 키자료(key material)은 유출이 안되면서 암호화작업에 사용이 가능하다. 시스템에서 키사용시가나 방법을 제한할수도 있다. 즉 키사용을 위해 사용자 인증을 요구하거나 특정암호화모드에서만 해당 키를 사용하도록 제한할수도 있다”

 

키저장소라는 이름에서 알수 있듯이 내가 사용하고자 하는 키를 안전하게 저장해주고 내가 사용하고 싶은때 꺼내서 사용할수 있다는것이다. 이제 키의 안정성에 대해 나는 완전히 안전하게 사용하고 있다고 당당하게 말할 수 있게 해줬다는 얘기이다.

 

아래 링크에 완전히 대체 가능한 코드샘플이 있다.

키를 RSA로 키쌍으로 생성하고 개인키로 암호화하고 공개키로 복호화하는 방식을 사용하였다.

 

https://gist.github.com/FrancescoJo/b8280cff14f1254f2185a9c2e927565e

 

import android.os.Build
import android.security.KeyPairGeneratorSpec
import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties
import android.util.Base64
import timber.log.Timber
import java.math.BigInteger
import java.security.GeneralSecurityException
import java.security.KeyPairGenerator
import java.security.KeyStore
import java.security.Security
import java.security.spec.RSAKeyGenParameterSpec
import java.security.spec.RSAKeyGenParameterSpec.F4
import java.util.*
import javax.crypto.Cipher
import javax.security.auth.x500.X500Principal

/**
 * String encryption/decryption helper with system generated key.
 *
 * Results generated by this class are very difficult but not impossible to break. Since Android is
 * easy to decompile and attacker knows how the key generation and usage is implemented. It means
 * replay attack is still possible so attackers have a reliable chance better than brute force.
 *
 * Therefore, do not plant any values on this class which maybe used as attack vectors - such as
 * unique device identifiers(MAC address, OS version, etc.), nano timestamp, constants not related
 * to cipher specifications, etc.
 *
 * @author Francesco Jo(nimbusob@gmail.com)
 * @since 25 - May - 2018
 */
object AndroidRsaCipherHelper {
    /** All inputs are must be shorter than 2048 bits(256 bytes) */
    private const val KEY_LENGTH_BIT = 2048

    // Let's think about this problem in 2043
    private const val VALIDITY_YEARS = 25

    private const val KEY_PROVIDER_NAME = "AndroidKeyStore"
    private const val CIPHER_ALGORITHM =
            "${KeyProperties.KEY_ALGORITHM_RSA}/" +
            "${KeyProperties.BLOCK_MODE_ECB}/" +
            KeyProperties.ENCRYPTION_PADDING_RSA_PKCS1

    private lateinit var keyEntry: KeyStore.Entry

    // Private only backing field
    @Suppress("ObjectPropertyName")
    private var _isSupported = false

    val isSupported: Boolean
        get() = _isSupported
    
    private lateinit var appContext: Context

    internal fun init(applicationContext: Context) {
        if (isSupported) {
            Timber.w("Already initialised - Do not attempt to initialise this twice")
            return
        }

        this.appContext = applicationContext
        val alias = "${appContext.packageName}.rsakeypairs"
        val keyStore = KeyStore.getInstance("AndroidKeyStore").apply({
            load(null)
        })

        val result: Boolean
        result = if (keyStore.containsAlias(alias)) {
            true
        } else {
            Timber.v("No keypair for %s, creating a new one", alias)

            if (Build.VERSION.SDK_INT > Build.VERSION_CODES.LOLLIPOP_MR1 && initAndroidM(alias)) {
                true
            } else {
                initAndroidL(alias)
            }
        }

        this.keyEntry = keyStore.getEntry(alias, null)
        _isSupported = result
    }

    private fun initAndroidM(alias: String): Boolean {
        try {
            with(KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_RSA, KEY_PROVIDER_NAME), {
                val spec = KeyGenParameterSpec.Builder(alias,
                        KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)
                        .setAlgorithmParameterSpec(RSAKeyGenParameterSpec(KEY_LENGTH_BIT, F4))
                        .setBlockModes(KeyProperties.BLOCK_MODE_CBC)
                        .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_RSA_PKCS1)
                        .setDigests(KeyProperties.DIGEST_SHA512,
                                KeyProperties.DIGEST_SHA384,
                                KeyProperties.DIGEST_SHA256)
                        /*
                         * Setting true only permit the private key to be used if the user authenticated
                         * within the last five minutes.
                         */
                        .setUserAuthenticationRequired(false)
                        .build()
                initialize(spec)
                generateKeyPair()
            })
            Timber.i("Random keypair with %s/%s/%s is created.", KeyProperties.KEY_ALGORITHM_RSA,
                    KeyProperties.BLOCK_MODE_CBC, KeyProperties.ENCRYPTION_PADDING_RSA_PKCS1)

            return true
        } catch (e: GeneralSecurityException) {
            /*
             * Nonsense, but some devices manufactured by developing countries have actual problem
             * Consider using JCE substitutes like Spongy castle(Bouncy castle for android)
             */
            Timber.w(e, "It seems that this device does not support RSA algorithm!!")

            return false
        }
    }

    /**
     * Tested and verified working on Nexus 5s API Level 21, it is not guaranteed that this logic is valid on
     * all Android L devices.
     */
    private fun initAndroidL(alias: String): Boolean {
        try {
            with(KeyPairGenerator.getInstance("RSA", KEY_PROVIDER_NAME), {
                val start = Calendar.getInstance(Locale.ENGLISH)
                val end = Calendar.getInstance(Locale.ENGLISH).apply { add(Calendar.YEAR, VALIDITY_YEARS) }
                val spec = KeyPairGeneratorSpec.Builder(appContext)
                        .setKeySize(KEY_LENGTH_BIT)
                        .setAlias(alias)
                        .setSubject(X500Principal("CN=francescojo.github.com, OU=Android dev, O=Francesco Jo, L=Chiyoda, ST=Tokyo, C=JP"))
                        .setSerialNumber(BigInteger.ONE)
                        .setStartDate(start.time)
                        .setEndDate(end.time)
                        .build()
                initialize(spec)
                generateKeyPair()
            })
            Timber.i("Random RSA algorithm keypair is created.")

            return true
        } catch (e: GeneralSecurityException) {
            Timber.w(e, "It seems that this device does not support encryption!!")

            return false
        }
    }

    /**
     * Beware that input must be shorter than 256 bytes. The length limit of plainText could be dramatically
     * shorter than 256 letters in certain character encoding, such as UTF-8.
     */
    fun encrypt(plainText: String): String {
        if (!_isSupported) {
            return plainText
        }

        val cipher = Cipher.getInstance(CIPHER_ALGORITHM).apply({
            init(Cipher.ENCRYPT_MODE, (keyEntry as KeyStore.PrivateKeyEntry).certificate.publicKey)
        })
        val bytes = plainText.toByteArray(Charsets.UTF_8)
        val encryptedBytes = cipher.doFinal(bytes)
        val base64EncryptedBytes = Base64.encode(encryptedBytes, Base64.DEFAULT)

        return String(base64EncryptedBytes)
    }

    fun decrypt(base64EncryptedCipherText: String): String {
        if (!_isSupported) {
            return base64EncryptedCipherText
        }

        val cipher = Cipher.getInstance(CIPHER_ALGORITHM).apply({
            init(Cipher.DECRYPT_MODE, (keyEntry as KeyStore.PrivateKeyEntry).privateKey)
        })
        val base64EncryptedBytes = base64EncryptedCipherText.toByteArray(Charsets.UTF_8)
        val encryptedBytes = Base64.decode(base64EncryptedBytes, Base64.DEFAULT)
        val decryptedBytes = cipher.doFinal(encryptedBytes)

        return String(decryptedBytes)
    }
}

 

앱은 재등록하였고 이번엔 통과되었다. ^^

 

 

Posted by 삼스
iOS2022. 7. 29. 10:38

iOS에서 mac과 xcode가 없이도 로그를 확인할 수 있도록 작성한 로거앱을 소개하겠다.

이 앱은 안드로이드와 달리 별도의 앱을 폰에 설치하여 로그를 확인할 수 있는 방법이 iOS에서는 제공되지 않는것인지 내가 못찾는것인지 찾을수가 없어서 그냥 내가 필요해서 만든 앱이다.

 

https://github.com/samse/SwiftSocketLogger

 

GitHub - samse/SwiftSocketLogger: Swift socket logger

Swift socket logger. Contribute to samse/SwiftSocketLogger development by creating an account on GitHub.

github.com

github에서 소스를 다운받아서 빌드하여 앱을 설치해야 한다. 스토어에서 설치 가능하도록 스토어게시를 시도해볼 생각이다.

위 소스로 앱을 설치하고 실행하면 아래 처럼 로그를 확인할 수 있다.

 

 

소켓으로 로그를 수신받아 로깅을 하는 방식이며 백그라운드에서 로그를 수집할 수 있어야 하기 때문에 백그라운드앱으로 작성되었다. 백그라운드 동작 방식은 무음 mp3를 재생하는 단순하고 쉬운 방식으로 작성하였다. 이겄 때문에 스토어게시가 안될수도 있겠단 생각이 들긴 한데 리젝되면 다른 방식으로 수정해볼 예정이다.

 

이제 로거 서버에 해당하는 로거앱을 설치하였으니 로그를 발생시키기 위해서 필요한 클라이언트앱에서는 어떻게 해야 하는지 설명하겠다.

github 페이지에서도 설명하였지만 일단 로거앱의 소스중에 swiftsocket 폴더의 소스를 클라이언트 앱으로 모두 가져와야 한다.

그리고 다음의 클라이언트용 로거 클래스를 추가한다.

 

class SocketLogger : NSObject {
    enum LoggerStatus: Int { case disconnected=0, connecting, connected }
    var status: LoggerStatus = .disconnected

    func sendLogs(_ logStr: String, completion: (() -> ())?) {
        if status == .connected {
            return
        }
        status = .connecting
        print("Logger sendLogs : \(logStr)")
        let client = TCPClient(address: "127.0.0.1", port: 2532)
        switch client.connect(timeout: 10) {
          case .success:
            status = .connected
            let data = logStr.data(using: .utf8)
            let _ = client.send(data: data!)
            DispatchQueue.global().asyncAfter(deadline: .now() + 0.2) {
                self.status = .disconnected
                if let completion = completion { completion() }
            }
          case .failure(let error):
            print("failure: \(error)")
            status = .disconnected
        }
    }
}

 

그리고 소켓 로거클래스를 사용하는 로거도 추가한다.

 

class Logger {
    static func debug(_ message: String?) {
        printLog(level: "🐛 DEBUG", message: message)
    }
    
    /// 경고 로그
    static func warning(_ message: String?) {
        printLog(level: "⚠️ WARNING", message: message)
    }
    
    /// 오류 로그
    static func error(_ message: String?) {
        printLog(level: "🚫 ERROR", message: message)
    }
    
    /// 정보 로그
    static func info(_ message: String?) {
        printLog(level: "ℹ️ INFO", message: message)
    }

    func printLog(level: String, message: String) {
        let logStr = "[\(level)] \(message ?? "")"
        self.appendLog(logStr)
    }
    
    static var logQueue = [String]()
    static let lock = NSLock()
    private static func appendLog(_ logStr: String) {
        print("\(logStr)")
        lock.lock(); defer { lock.unlock() }
        logQueue.append(logStr)
        triggerLogging()
    }
    private static func getLog() -> String? {
        if logQueue.count > 0 {
            return logQueue.first
        }
        return nil
    }
    private static func removeLog() {
        if logQueue.count > 0 {
            logQueue.removeFirst()
        }
    }
    
    static var socketLogger: SocketLogger = SocketLogger()
    private static func triggerLogging() {
        if let logStr = getLog() {
            socketLogger.sendLogs(logStr) {
                removeLog()
                triggerLogging()
            }
        }
    }
}

 

이제 앱코드내에서 다음과 같이 로거를 호출하면 SwiftSocketLogger앱에 해당 로그가 나오게 된다.

Logger.debug("my log" )
Logger.info("my info log")

아래 그림에 보는것과 같이 우상단의 디버그레벨 문구를 누르면 디버그레벨을 변경이 가능하며 좌측의 텍스트필드에 값을 입력하면 해당 값으로 로그를 필터링해서 볼수 있다.

 

 

단말만으로 디버깅을 해야 하는 상황에서 유용하게 사용할 만한 방법이라고 생각되며 잘 사용하고 있는 앱이다 다른 개발자들도 일부 불편함을 해소하는데 사용되었으면 좋겠다.

 

Posted by 삼스
iOS2022. 7. 28. 16:41

가볍고 단순한 안드로이드와 iOS를 위한 하이브리드 라이브러리를 소개하겠다.

 

iOS용

https://github.com/samse/WKBridgeKit

 

GitHub - samse/WKBridgeKit

Contribute to samse/WKBridgeKit development by creating an account on GitHub.

github.com

 

안드로이드용

https://github.com/samse/NBridgeKit

 

GitHub - samse/NBridgeKit

Contribute to samse/NBridgeKit development by creating an account on GitHub.

github.com

 

아주 가볍게 하이브리드앱을 개발할 수 있도록 해준다.

하이브리드앱은 웹에서 javascript로 native기능을 사용할 수 있게 해주는 플러그인을 제공해주어야 하는데 이 때 사용되어야 하는 javascript core는 아래 위치에서 찾을 수 있다.

 

https://github.com/samse/WKBridgeKit/tree/main/Sample/Sample/www

 

위 위치에서 sample.js를 열어보면 어떻게 플러그인을 사용가능한지 확인 할 수 있다.

 

앱정보를 읽어오는 javascript code샘플은 다음과 같다.

    nbridge.app.appInfo().then(function(result) {
        alert('app version : ' + result.version + "/" + result.build + "\napp name: " + result.name);
    }, function(error) {
        alert(error);
    })

애플 앱스토어에 샘플앱이 게시되어 있으니 한번씩 설치해서 확인해 보는것이 도움이 될것 같다.

 

https://apps.apple.com/kr/app/nbridge/id1628984500

 

‎nBridge

‎Swift기반으로 WKWebView와 Native를 연동할 수 있는 기본기능을 제공합니다. 아래 사이트에서 자세한 정보를 얻을 수 있으며 이를 기반으로 아주 가벼운 Hybrid앱을 작성할 수 있습니다. https://github.c

apps.apple.com

 

 

하이브리드 기술이 일반화가 많이 되었긴 하지만 가볍게 사용가능한 라이브러리가 있으면 좋겠다는 생각에 정리하여 배포하였다. 다른 이들에게 도움이 조금이라도 되면 보람이 될것 같다.

소스를 보면 플러그인을 추가하는 방법도 알수 있겠지만 쉽게 설명하는 글을 한번 더 정리해볼 예정이다.

Posted by 삼스
iOS2022. 6. 15. 10:24

SceneDelegate? 멀티윈도우?

AppDelegate로 관리되던게 어느순간 SceneDelegate란게 나타났다.
iOS13부터 등장한거 같은데...
SI특성상 iOS11부터 지원하는 경우가 많아서 아직까지 SceneDelegate를 사용하는 경우가 없긴 하나 알아두어야 하겠다.

AppDelegate는 iOS12와 이전버전에서 앱의 라이프사이클을 관리하는데 사용된다.
iOS13부터는 일부 역할을 SceneDelegate에 넘겨주게 되며 AppDelegate의 역할은 자연히 줄게 되었다.
AppDelegate는 여전히 앱이 시작되는 시작점을 비롯하여 앱레벨의 라이프사이클을 관리한다.

주요한 3개를 정리해 보면 
1. func application(_: didFinishLaunchingWithOptions:) -> Bool
앱 시작 포인트

2. func application(_: configurationForCOnnecting:options:)-> UISceneConfiguration
새로운 scene/window를 제공하려고 할때 호출됨. 최초에는 호출되지 않음

3. func application(_: didDiscardSceneSessions:)
scene이 종료될때 호출. 사용자가 multitasking이나 코드레벨에서 없애는 경우가 해당된다.


SceneDelegate

화면에 무엇을 보여줄것인지 처리한다.

1. scene(_: willConnectTo:options:)
scene은 하나의 UISceneSession을 갖게 되고 여기에서 처음 이 메서드가 호출된다.
contentView, window생성 그리고 window의 rootViewController를 설정한다.
storyboard를 사용하지 않고 개발하는 경우 여기서 UIWindow를 생성하고 rootViewController생성하는 코드를 직접 작성해야 한다.
storyboard를 사용한다면 특별히 작성해야 할 코드는 없다.

2. sceneWillEnterForeground(_:)
scene이 foreground로 전환될때 호출된다.

3. sceneDidBecomeActive(_:)
scene이 setup되고 화면에 표시될 준비가 완료된 상태에 호출된다.

4. sceneWillResignActive(_:)
active -> inactive 시 호출된다.

5. sceneDidEnterBackground(_:)
foreground -> background시 호출

6. sceneDidDisconnect(_:)
background상태에서 시스템이 자원을 확보하기 위해서 scene을 disconnect할 수 있다.

그렇다면 왜 이렇게 바꿨을까에 대한 생각을 안해볼수가 없지 않나?

이유는 iPad에서 Multiwindow를 지원하면서 나타났다.

멀티윈도우를 지원하려다 보니 AppDelegate가 하나의 UI를 관리하던 체제에서 여러 UI를 관리히야 하게 되었고 이 때 UI를 Scene단위로 구분하게 된것이다. 따라서 AppDelegate는 UI관련 처리를 하지 않게 되었고 이런 부분은 SceneDelegate에서 해야 하게 되었다.

멀티윈도우를 지원하지 않을거라면 궂이 SceneDelegate를 사용할 필요가 없다는 것이다.
SceneDelegate를 삭제하는 방법은 다음과 같다.

1. SceneDelegate.swift 삭제
2. UIWIndow를 AppDelegate로 이동
3. Info.plist에서 Application Scene Manifest삭제

AppDelegate는 UI관련 처리를 하지 않는 대신에 SceneSession이 추가되거나 삭제될때 보고를 받게 된다.

Scene은 안드로이드의 Activity가 시스템에 의하여 수거될수 있는것처럼 앱이 background상태일 때 수거될 수 있다.
이 때 해당 화면의 데이터를 보관했다가 나중에 Scene이 다시 새로 생성될 때 화면에 복원해주어야 할 수 있다.
이를 위해 func stateRestorationActivity(for scene: UIScene) -> NSUserActivity?를 제공하며 여기서 관심있는 데이터를 저장했다가 func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) 호출 시 저장했던 데이터로 화면을 복원할 수 있다.

SceneDelegate가 멀티윈도우를 지원하기 위해서 나왔고 따라서 ViewController가 하나만 생성되던 상황이 아닐수 있게 되었다. 이 때 고전적으로 ViewController에서 어떤 데이터가 생성되는 이벤트가 발생하면 그 내용을 화면에 바로 반영하는 방식으로 작업하게 되면 하나의 화면에만 그 내용이 반영되는 경우가 발생할 수 있다.
이를 해결하기 위해서는 데이터가 발생하면 모든 뷰컨트롤러에서 이를 처리하는 방식으로 수정되어야 한다.
3가지정도의 방법이 있다.

1. delegate로 이벤트 발생 시 모두처리
2. notification으로 이벤트 발생 시 모두 처리
3. Swift Combine Framework 사용

notification으로 처리하는것을 예를 들면 데이터가 발생하는 이벤트가 발생하면 바로 화면에 반영하지 않고 이벤트는 모델 컨트롤러에 저장하고 notification을 생성하여 전송한다. ViewController는 notification handler를 작성하고 여기서 모델컨트롤러의 데이터에 접근하여 화면에 반영한다. 그러면 모든 ViewController가 데이터의 변화를 감지하고 화면에 반영할수 있다.

이제 멀티윈도우이벤트를 발생시켜서 두개의 화면으로 나뉘게 하는 방법을 알아볼 차례인데...
아직 이런걸 만들어 볼일이 없어서 나중에 필요가 생기면 알아보기로 하겠다.

Posted by 삼스