글쓴이 보관물: mcsong

mcsong

mcsong에 대하여

Software Engineer, 카산드라 완벽 가이드(http://www.yes24.com/24/goods/5847220) 번역

웹뷰(WebView) 캐시 설정

종종 멀티 플랫폼을 지원하는 서비스를 사용해 보면, 앱에서 웹뷰(WebView)를 사용해서 서버의 페이지를 보여주는 경우를 쉽게 볼 수 있다. 하지만, 네트웍이 안 되는 경우에는 아래와 같은 에러 페이지를 볼 수 있다. 사용자 관점에서 이 화면은 아주 안 좋은 경험(UX)을 줄 수 있고, 이 경우를 개선하기 위해서 네트웍을 사용할 수 없는 경우에 마지막에 본 페이지를 캐시해서 보여주기도 한다.

그래서, 네트웍을 사용할 수 없는 경우, 위의 에러 화면이 아니라 마지막으로 본 페이지를 보여주는 방법을 간단하게 살펴보면 아래와 같다.

public static void setWebView(Context context) {
	webView.getSettings().setAppCachePath(getContext().getCacheDir().getAbsolutePath());
	webView.getSettings().setAllowFileAccess(true);
	webView.getSettings().setAppCacheEnabled(true);
	webView.getSettings().setCacheMode(WebSettings.LOAD_DEFAULT);

	// 네트웍 연결여부를 확인한다. 
	if(!isNetworkAvailable(context)) {
	  webView.getSettings().setCacheMode( WebSettings.LOAD_CACHE_ELSE_NETWORK );
	}
}

public static boolean isNetworkAvailable(Context context) {
  ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
  NetworkInfo netInfo = cm.getActiveNetworkInfo();
  return netInfo != null && netInfo.isConnected();
}

안드로이드 화면에 실행중인 앱 확인하는 방법

안드로이드 5(롤리팝) 이후부터는 안드로이드 기기에 실행중인 앱을 알 수 없게 되었습니다. 그래서, 안드로이드 5 이후 버전에서는 기기에 저장하고 있는 통계 데이터를 기반으로 완전히 정확하지는 않지만, 대략적으로 정확한 정보를 가져올 수 있습니다. UsageStatsManager를 사용해서 현재 안드로이드 기기의 화면에 실행중인 앱을 검색하는 방법을 살펴보자.

아래는 UsageStatsManager를 사용해서 안드로이드 기기에 저장되어 있는 이벤트 데이터를 검색하고, 이벤트가 포그라운드(Event.MOVE_TO_FOREGROUND 또는 Event.ACTIVITY_RESUMED)인 것을 확인해서 마지막 포그라운드 이벤트에 해당하는 패키지 이름을 가져온다. 이 정보는 대체로 정확하고, 아래 코드로 가져온 패키지가 현재 화면에서 실행중인 것을 확인 할 수 있다.

1. 앱의 AndroidManifest.xml에 아래의 권한을 추가


2. 안드로이드 기기에서 실행중인 앱 가져오기

public static String getTopPackageName(@NonNull Context context) {
  UsageStatsManager usageStatsManager = (UsageStatsManager) context.getSystemService(Context.USAGE_STATS_SERVICE);

  long lastRunAppTimeStamp = 0L;

  final long INTERVAL = 1000 * 60 * 5;
  final long end = System.currentTimeMillis();
  // 1 minute ago
  final long begin = end - INTERVAL;

  LongSparseArray packageNameMap = new LongSparseArray<>();
  final UsageEvents usageEvents = usageStatsManager.queryEvents(begin, end);
  while (usageEvents.hasNextEvent()) {
    UsageEvents.Event event = new UsageEvents.Event();
    usageEvents.getNextEvent(event);

    if(isForeGroundEvent(event)) {
      packageNameMap.put(event.getTimeStamp(), event.getPackageName());
      if(event.getTimeStamp() > lastRunAppTimeStamp) {
        lastRunAppTimeStamp = event.getTimeStamp();
      }
    }
  }

  return packageNameMap.get(lastRunAppTimeStamp, "");
}

3. 이벤트가 포그라운드 이벤트인지 확인

private static boolean isForeGroundEvent(UsageEvents.Event event) {
  if(event == null) {
    return false;
  }

  if(BuildConfig.VERSION_CODE >= 29) {
    return event.getEventType() == UsageEvents.Event.ACTIVITY_RESUMED;
  }

  return event.getEventType() == UsageEvents.Event.MOVE_TO_FOREGROUND;
}

위 코드를 사용해서 완전히 정확하지는 않지만, 대체로 정확하게 안드로이드 화면에서 실행중인 앱을 알 수 있다.

안드로이드 컨텍스트(Context)

안드로이드 컨텍스트(Context)는 리소스에 접근하는 인터페이스이고, 이 인터페이스는 크게 애플리케이션(Application), 액티비티(Activity), 컨텍스트 래퍼(ContextWrapper)의 베이스 컨텍스트(BaseContext)로 3가지 형태를 가진다. 안드로이드를 개발하는 데 있어 특정 상황에서 어떤 컨텍스트를 사용해야 하는지 아는 것은 매우 중요하다. 그래서, 개별 컨텍스트에 대해서 살펴보자.

애플리케이션 컨텍스트
– 애플리케이션은 컨텍스트는 앱 프로세스 자체라고 생각하면 쉽다. 애플리케이션 클래스 자체가 애플리케이션이기 때문에 안드로이드 앱 전반에 걸쳐서 사용할 수 있다. 그리고, 애플리케이션 클래스가 애플리케이션 컨텍스트이기에 라이프 사이클이 애플리케이션하고 같다. getApplicationContext()나 Application 클래스를 상속한 클래스 객체를 사용하면 된다.

액티비티
– 액티비티 컨텍스트도 액비티비 자체라고 생각하면 쉽다. 프레그먼트(Fragment)나 뷰에서 사용할 수 있는 컨텍스트이다. 일반적으로 안드로이드 뷰와 관련된 작업을 하는 데 사용한다. 액티비티 컨텍스트로 액티비티 자체이기에 라이프 사이클이 액티비티와 같다. 보통 액티비티에서는 this, 프레그먼트에서는 getContext()나 getActivity(), 그리고 뷰에서는 getContext()로 컨텍스트를 사용하면 된다.

베이스 컨텍스트
– 자신의 컨텍스트가 아닌 다른 컨텍스트를 접근하기 위해서 사용한다. 보통 getBaseContext()로 컨텍스트를 사용하면 된다.  [ 그림 1. 컨텍스트 클래스 다이어그램 ]

위 그림은 컨텍스트를 어떤 안드로이드 구성요소가 구현하고 있는지 알 수 있고, 위에서 설명한 컨텍스트 객체가 왜 애플리케이션, 액티비티, 컨텍스트 래퍼의 베이스 컨텍스트인지 알 수 있다.

스택오버플로어 사이트에서, 안드로이드 컨텍스트와 사용할 수 있는 기능들을 정리한 이미지를 살펴보면 다음과 같다.


[ 그림 2. 컨텍스트 ]


[ 그림 3. 개별 컨텍스트에서 사용할 수 있는 기능들 ]

안드로이드에서 제공하는 컨텍스트에 대해서 살펴봤다. 그리고 개별 컨텍스트가 제공할 수 있는 기능별 제약이 있기에, 잘 알아서 여러 상황에 맞는 컨텍스트를 사용해야 한다.

레퍼런스
https://stackoverflow.com/questions/3572463/what-is-context-on-android

VM, 원격 PC 연결 클라이언트 개발에 참고할 만한 프로젝트들

이 글의 내용은, 가상화 프로젝트를 하면서 윈도, 안드로이드 앱을 개발하면서 사용하거나 살펴본 오픈소스 및 바이너리에 대한 정리이다. VM(가상화된 PC)이나 원격 PC에 접근하는데 많이 알려진 RDP(Remote Desktop Protocol), SPICE(Simple Protocol for Independent Computing Environments), 그리고 VNC(Virtual Network Computing) 프로토콜을 주로 사용한다. 이 프로토콜은 원격 PC 화면을 스트리밍하기 위해서 개발되었다.

* RDP
RDP의 경우에는 마이크로소프트에서 개발한 프로토콜이고, 거의 모든 윈도가 이 프로토콜을 구현한 클라이언트인 mstsc.exe가 번들로 배포된다. 그리고, 윈도 7에서 성능을 향상한 RemoteFX와 같은 기능을 사용하기 위해서는 별도로 업데이트할 수 있다.
– 프로토콜 스펙 : https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/5073f4ed-1e93-45e1-b039-6e30c385867c

* SPICE 프로토콜
SPICE 프로토콜의 경우에는 오픈소스 프로젝트이다. 이 프로젝트는 레드햇(RedHat, 2007년에 Qumranet 회사 인수)에서 소스를 오픈 했다.
– SPICE 프로토콜 스펙 : https://www.spice-space.org/static/docs/spice_protocol.pdf

* VNC 프로토콜
VNC 프로토콜의 경우에는 성능 문제가 있어서 제외했다.

레드햇이 가상화 솔루션을 오픈소스로 열어서 oVirt(https://ovirt.org/)라는 프로젝트가 진행중이고, 오픈소스 가상화 솔루션 중에서는 가장 발전한 프로젝트이다. 그리고, 레드햇에서 제품으로 판매하는 가상화 솔루션도 oVirt를 기반하고 있다.

이제 이 프로토콜을 기반으로 개발하는 데 도움이 되는 프로젝트를 살펴보자.

* HTML 클라이언트
웹 소켓을 사용해서 원격 PC의 화면을 스트리밍한다. 아래 프로젝트를 기반으로 웹 앱을 개발할 수 있다.

– Guacamole : RDP와 VNC를 지원하는 아파치 프로젝트로, https://guacamole.apache.org/ 에서 확인할 수 있다.
– SpiceHtml5 : SPICE를 스트리밍하는 자바스크립트 프로젝트로, https://github.com/freedesktop/spice-html5 에서 확인할 수 있다.
– Myrtille : .NET 기반의 오픈소스 프로젝트로, https://cedrozor.github.io/myrtille 에서 확인할 수 있다.

* 윈도
윈도에서 RDP와 SPICE를 지원하는 프로젝트아래의 프로젝트를 사용해서 윈도에서 RDP와 SPICE를 지원할 수 있다.
– mstsc : 윈도 내장 RDP 클라이언트이다. 이 툴은 mstsc.exe 실행 옵션이나 rdp 파일을 사용하면 된다.
– virt viewer : 윈도에서 SPICE 서버에 연결하도록 지원한다. 이 툴은 https://releases.pagure.org/virt-viewer/ 에서 다운로드할 수 있고, https://pagure.io/virt-viewer/blob/master/f/NEWS 에서 Changelogs를 확인할 수 있다.
– FreeRDP : RDP 프로토콜을 지원하는 FreeRDP 프로젝트로, 소스는 https://github.com/FreeRDP/FreeRDP/tree/master/client/Windows 에서 확인할 수 있다.

* 맥
아래 프로젝트로 맥 클라이언트를 개발할 수 있다.
– FreeRDP : RDP 프로토콜을 지원하는 FreeRDP 프로젝트로, 소스는 https://github.com/FreeRDP/FreeRDP/tree/master/client/Mac 에서 확인할 수 있다.

* 안드로이드
안드로이드에서 RDP와 SPICE를 지원하는데 사용할 수 있는 프로젝트들이다.

– freerdp : RDP 기반의 오픈소스 프로젝트로, https://github.com/FreeRDP/FreeRDP/tree/master/client/Android 에서 확인할 수 있다.
– flexVDI : flexVDI에서 오픈한 SPICE 프로젝트로, https://github.com/flexVDI/launcher-mobile 에서 확인할 수 있다.

– VirtualDesktop : SPICE 프로토콜을 지원하는 안드로이드 앱 프로젝트, https://github.com/pisceslj/VirtualDesktop 에서 확인할 수 있다.

– oVirt 매니저 앱 : oVirt 서버에 접근해서 관리할 수 있는 기능을 제공하는 안드로이드 앱으로, https://github.com/oVirt/moVirt 에서 확인할 수 있다.

* iOS
iOS 에서 RDP와 SPICE를 지원하기 위해서 사용할 수 있는 프로젝트들이다.
– freerdp : RDP 기반의 오픈 소스 프로젝트로, https://github.com/FreeRDP/FreeRDP/tree/master/client/iOS 에서 확인할 수 있다.
– flexVDI : flexVDI에서 오픈한 SPICE iOS 오픈소스 앱으로, https://github.com/flexVDI/launcher-mobile 에서 확인할 수 있다.

개인적으로 RDP와 SPICE 위주로 윈도와 안드로이드 앱을 개발해 본 경험으로, 원격 프로토콜을 성능 관점으로 보면 RDP > SPICE > VNC 순이다. SPICE의 경우에는 꽤 괜찮은 성능을 보여주지만, 그에 반해서 리소스(클라이언트에서 서버에 다수의 소켓을 열어서 성능을 개선)를 많이 사용한다. 결론으로 원격 프로토콜은 RDP 쵝오 !!

* Reference
https://en.wikipedia.org/wiki/Simple_Protocol_for_Independent_Computing_Environments

안드로이드 앱이 동작하는 폼 팩터(Form Factor) 확인 방법

폼 팩터(Form Factor)의 정의를 위키피디아에서 살펴보면 아래와 같다.

컴퓨터 시스템의 각 부품의 물리적 치수의 형태를 의미한다.

즉, 소프트웨어가 동작하는 기기의 형태를 말하는 것이다. 안드로이드 적용범위가 확대되면서, 안드로이드 앱이 실행할 수 있는 하드웨어 기기가 많아졌다. 예로, 크롬북이나 TV등을 예로 들 수 있다.

최근에 안드로이드 앱을 크롬북과 삼성 덱스(DEX)의 UI/UX에 어울리도록 작업을 하면서 특정 기능(키보드 등)은 모바일/태블릿에서만 필요하다. 그래서 앱이 동작하는 기기의 정보를 확인해야 했고, 아래의 코드로 해결을 했다.

아래 코드는 구글 TalkBack 깃헙 사이트의 https://github.com/google/talkback/blob/master/utils/src/main/java/FormFactorUtils.java 코드에 삼성 덱스(DEX)를 확인하는 코드를 추가했다.

아래의 ARC는 App Runtime for Chrome의 줄임말으로 폼 팩터가 크롬북이라는 것을 알 수 있다.

import android.app.UiModeManager;
import android.content.Context;
import android.content.pm.PackageManager;
import android.content.res.Configuration;
import android.os.Build;

public class FormFactorUtils {
	private static final int FORM_FACTOR_PHONE_OR_TABLET = 0;
	private static final int FORM_FACTOR_WATCH = 1;
	private static final int FORM_FACTOR_TV = 2;
	private static final int FORM_FACTOR_ARC = 3;

	private static final int FORM_FACTOR_SAMSUNG_DEX = 100;

	private static final String ARC_DEVICE_PATTERN = ".+_cheets|cheets_.+";

	private static FormFactorUtils sInstance;

	private final int mFormFactor;
	private final boolean mHasAccessibilityShortcut;

	private FormFactorUtils(final Context context) {
		// Find device type.
		if (context.getApplicationContext()
				.getPackageManager()
				.hasSystemFeature(PackageManager.FEATURE_WATCH)) {
			mFormFactor = FORM_FACTOR_WATCH;
		} else if (isContextTelevision(context)) {
			mFormFactor = FORM_FACTOR_TV;
		} else if (Build.DEVICE != null && Build.DEVICE.matches(ARC_DEVICE_PATTERN)) {
			mFormFactor = FORM_FACTOR_ARC;
		} else if(isContextDex(context)) {
			mFormFactor = FORM_FACTOR_SAMSUNG_DEX;
		} else {
			mFormFactor = FORM_FACTOR_PHONE_OR_TABLET;
		}

		// Find whether device supports accessibility shortcut.
		mHasAccessibilityShortcut = (BuildVersionUtils.isAtLeastO() && mFormFactor == FORM_FACTOR_PHONE_OR_TABLET);
	}

	/** @return an instance of this Singleton. */
	public static synchronized FormFactorUtils getInstance(final Context context) {
		if (sInstance == null) {
			sInstance = new FormFactorUtils(context);
		}
		return sInstance;
	}

	/** Return the cached version of the isWatch. */
	public boolean isWatch() {
		return mFormFactor == FORM_FACTOR_WATCH;
	}

	public boolean isArc() {
		return mFormFactor == FORM_FACTOR_ARC;
	}

	public boolean isTv() {
		return mFormFactor == FORM_FACTOR_TV;
	}

	public boolean isPhoneOrTablet() {
		return mFormFactor == FORM_FACTOR_PHONE_OR_TABLET;
	}

	public boolean isDex() {
		return mFormFactor == FORM_FACTOR_SAMSUNG_DEX;
	}

	public boolean hasAccessibilityShortcut() {
		return mHasAccessibilityShortcut;
	}

	public static boolean useSpeakPasswordsServicePref() {
		return BuildVersionUtils.isAtLeastO();
	}

	public static boolean isContextTelevision(Context context) {
		if (context == null) {
			return false;
		}

		UiModeManager modeManager = (UiModeManager) context.getSystemService(Context.UI_MODE_SERVICE);
		return modeManager != null
				&& modeManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_TELEVISION;
	}

	private static boolean isContextDex(Context context) {
		if (context == null) {
			return false;
		}

		try {
			Configuration config = context.getResources().getConfiguration();
			Class configClass = config.getClass();
			if (configClass.getField("SEM_DESKTOP_MODE_ENABLED").getInt(configClass)
					== configClass.getField("semDesktopModeEnabled").getInt(config)) {
				return true;
			}
		} catch (Exception e) {
			// ignore
		}

		return false;
	}
}