06. Custom 개발
Splash Window
HONE Smart Platform에서는 Splash Window 를 직접 관리하지 않으며 Splash Window 를 위한 예제를 샘플에 포함한다.
Splash Window 를 띄울 때는 onCreate 에서 호출하면 되기 때문에 별다른 주의점은 필요하지 않다.
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
ActionBase.setOnActionFlowEnd(::hideSplash)
showSplash()
}
fun showSplash() {
mSplashManager = SplashManager()
mSplashManager.show(findViewById(R.id.base_layout))
}
fun hideSplash() {
mSplashManager.hide(mDisposable)
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ActionBase.setOnActionFlowEnd(this::hideSplash);
showSplash();
}
private void showSplash() {
mSplashManager = new SplashManager();
mSplashManager.show(findViewById(R.id.base_layout));
}
public void hideSplash() {
if (mSplashManager != null) {
mSplashManager.hide(mDisposable);
}
}
Example (XML)
<android.support.constraint.ConstraintLayout
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/hone_main_splash"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/hsp_splash"
android:clickable="true"
tools:ignore="KeyboardInaccessibleWidget,Overdraw">
<ImageView
android:id="@+id/logo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_centerHorizontal="true"
android:layout_marginTop="50dp"
android:background="@drawable/logo"
app:srcCompat="@drawable/logo"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
/>
</android.support.constraint.ConstraintLayout>
</layout>
원하는 형태로 임의로 띄우면 되며 예제로 들어가는 세부 코드와 다르게 작성해도 무방하다.
이후 Splash 를 닫기 위해 Action 의 종료 시점에 hideSplash 를 등록하면 된다.
Dialog 커스텀
HONE Smart Platform 내에 사용 중인 Alert / Confirm / Progress 다이얼로그들은 DialogDelegate 를 이용하여 커스텀 할 수 있다.
AlertDialog
AlertDialog 는 AlertDialogBase 를 extends 하여 개발해야 하며 그에 대한 예제는 아래와 같다.
Example
companion object {
private val mLog = org.slf4j.LoggerFactory.getLogger(CustomAlertDialog::class.java)
val TYPE_A_DLG = 1
val TYPE_B_DLG = 2
}
override fun show(params: Dialog): AlertDialog? {
when (params.type) {
else -> return showDefault(params)
}
}
private fun showDefault(params: Dialog): AlertDialog? {
mWeakActivity.get()?.let {
val alertBuilder = AlertDialog.Builder(it)
params.title?.let { alertBuilder.setTitle(it) }
params.message?.let { alertBuilder.setMessage(it) }
alertBuilder.setCancelable(params.cancelable)
if (params.view != null) {
alertBuilder.setView(params.view)
val dlg = alertBuilder.create()
dlg.show()
return dlg
}
it.runOnUiThread {
val positiveText = if (TextUtils.isEmpty(params.positiveText))
it.getString(R.string.button_ok)
else
params.positiveText
alertBuilder.setPositiveButton(positiveText) { dialog, _ ->
params.listener?.onResult(OnResultListener.TRUE, dialog)
dialog.dismiss()
if (params.processKill) {
processKill()
}
if (params.finish) {
finish()
}
}
alertBuilder.show()
}
}
return null
}
}
private static final org.slf4j.Logger mLog = org.slf4j.LoggerFactory.getLogger(CustomAlertDialog.class);
public static final int TYPE_A_DLG = 1;
public static final int TYPE_B_DLG = 2;
public CustomAlertDialog(Activity activity) {
super(activity);
}
@Nullable
@Override
public AlertDialog show(@NonNull Dialog params) {
switch (params.type) {
default:
return showDefault(params);
}
}
private AlertDialog showDefault(@NonNull Dialog params) {
if (mWeakActivity.get() == null) {
mLog.error("ERROR: mWeakActivity.get() == null");
return null;
}
AlertDialog.Builder alertBuilder = new AlertDialog.Builder(mWeakActivity.get());
if (!TextUtils.isEmpty(params.title)) {
alertBuilder.setTitle(params.title);
}
if (!TextUtils.isEmpty(params.message)) {
alertBuilder.setMessage(params.message);
}
alertBuilder.setCancelable(params.cancelable);
if (params.view != null) {
alertBuilder.setView(params.view);
AlertDialog dlg = alertBuilder.create();
dlg.show();
return dlg;
}
mWeakActivity.get().runOnUiThread(() -> {
String positiveText = TextUtils.isEmpty(params.positiveText) ?
mWeakActivity.get().getString(R.string.button_ok) : params.positiveText;
alertBuilder.setPositiveButton(positiveText, (dialog, which) -> {
if (params.listener != null) {
params.listener.onResult(OnResultListener.TRUE, dialog);
}
dialog.dismiss();
if (params.processKill) {
processKill();
}
if (params.finish) {
finish();
}
});
alertBuilder.show();
});
return null;
}
}
AlertDialog 의 구현이 완료 되었으면 이를 DialogDelegate 에 해당 클래스 정보를 설정해야 하며 이는 MainActivity 의 onCreate 에 super 를 호출하기 전 설정되어야지만 올바르게 작동 된다.
Example
DialogDelegate.get().setClassPath(DialogDelegate.DialogType.ALERT,
"$packageName.dialog.CustomAlertDialog")
super.onCreate(savedInstanceState)
}
protected void onCreate(Bundle savedInstanceState) {
DialogDelegate.get().setClassPath(DialogDelegate.DialogType.ALERT,
getPackageName() + ".dialog.CustomAlertDialog");
super.onCreate(savedInstanceState);
}
ConfirmDialog
ConfirmDialog 의 경우 ConfirmDialogBase 를 상속하여 개발해야 하며 그에 대한 예제는 다음과 같다.
Example
companion object {
private val mLog = org.slf4j.LoggerFactory.getLogger(CustomConfirmDialog::class.java)
val TYPE_A_DLG = 1
val TYPE_B_DLG = 2
}
override fun show(params: Dialog): AlertDialog? {
when (params.type) {
else -> return showDefault(params)
}
}
private fun showDefault(params: Dialog): AlertDialog? {
mWeakActivity.get()?.let {
val alertBuilder = AlertDialog.Builder(it)
params.title?.let { alertBuilder.setTitle(it) }
params.message?.let { alertBuilder.setMessage(it) }
alertBuilder.setCancelable(params.cancelable)
if (params.view != null) {
alertBuilder.setView(params.view)
val dlg = alertBuilder.create()
dlg.show()
return dlg
}
it.runOnUiThread {
val positiveText = if (params.positiveText.isNullOrEmpty())
it.getString(honemobile.android.core.R.string.common_confirm)
else
params.positiveText
val negativeText = if (params.negativeText.isNullOrEmpty())
it.getString(honemobile.android.core.R.string.common_cancel)
else
params.negativeText
alertBuilder.setPositiveButton(positiveText) { dialog, _ ->
params.listener?.onResult(OnResultListener.TRUE, dialog)
dialog.dismiss()
}
alertBuilder.setNegativeButton(negativeText) { dialog, _ ->
params.listener?.onResult(OnResultListener.FALSE, dialog)
dialog.dismiss()
}
alertBuilder.setOnCancelListener { dialog ->
params.listener?.onResult(OnResultListener.FALSE, dialog)
}
alertBuilder.show()
}
}
return null
}
}
private static final org.slf4j.Logger mLog = org.slf4j.LoggerFactory.getLogger(CustomConfirmDialog.class);
public static final int TYPE_A_DLG = 1;
public static final int TYPE_B_DLG = 2;
public CustomConfirmDialog(Activity activity) {
super(activity);
}
@Override
public @Nullable
AlertDialog show(@NonNull Dialog params) {
switch (params.type) {
default:
return showDefault(params);
}
}
private AlertDialog showDefault(@NonNull Dialog params) {
if (mWeakActivity.get() == null) {
mLog.error("ERROR: mWeakActivity == null");
return null;
}
AlertDialog.Builder alertBuilder = new AlertDialog.Builder(mWeakActivity.get());
if (!TextUtils.isEmpty(params.title)) {
alertBuilder.setTitle(params.title);
}
if (!TextUtils.isEmpty(params.message)) {
alertBuilder.setMessage(params.message);
}
alertBuilder.setCancelable(params.cancelable);
if (params.view != null) {
alertBuilder.setView(params.view);
AlertDialog dlg = alertBuilder.create();
dlg.show();
return dlg;
}
mWeakActivity.get().runOnUiThread(() -> {
String positiveText = TextUtils.isEmpty(params.positiveText) ?
mWeakActivity.get().getString(honemobile.android.core.R.string.common_confirm) : params.positiveText;
String negativeText = TextUtils.isEmpty(params.negativeText) ?
mWeakActivity.get().getString(honemobile.android.core.R.string.common_cancel) : params.negativeText;
alertBuilder.setPositiveButton(positiveText, (dialog, which) -> {
if (params.listener != null) {
params.listener.onResult(OnResultListener.TRUE, dialog);
}
dialog.dismiss();
});
alertBuilder.setNegativeButton(negativeText, (dialog, which) -> {
if (params.listener != null) {
params.listener.onResult(OnResultListener.FALSE, dialog);
}
dialog.dismiss();
});
alertBuilder.setOnCancelListener(dialog -> {
if (params.listener != null) {
params.listener.onResult(OnResultListener.FALSE, dialog);
}
});
alertBuilder.show();
});
return null;
}
}
구현이 완료되었으면 AlertDialog 와 동일한 방식으로 클래스 정보를 설정해야 한다.
Example
DialogDelegate.get().setClassPath(DialogDelegate.DialogType.CONFIRM,
"$packageName.dialog.CustomConfirmDialog")
super.onCreate(savedInstanceState)
}
protected void onCreate(Bundle savedInstanceState) {
DialogDelegate.get().setClassPath(DialogDelegate.DialogType.CONFIRM,
getPackageName() + ".dialog.CustomConfirmDialog");
super.onCreate(savedInstanceState);
}
LoadingDialog
LoadingDialog 는 LoadingDialogBase 를 extends하여 구현해야 하며 그에 대한 예제는 아래와 같다.
첫번째 예제는 HSP 에서 제공하는 XML 에 레이아웃만 변경하여 사용할 경우 의 예제 이다.
Example
// xml 의 id 는 동일하게 사용하고 xml 내 view 의 위치가 다를때
override fun layoutId() =
R.layout.custom_loading_dialog
}
public CustomLoadingDialog(Activity activity) {
super(activity);
}
// xml 의 id 는 동일하게 사용하고 xml 내 view 의 위치가 다를때
@Override
protected int layoutId() {
return R.layout.custom_loading_dialog;
}
}
XML Example
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/loading_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="20dp"
>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/spinner_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
>
<ProgressBar
android:id="@+id/spinner_progress"
style="?android:attr/progressBarStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
/>
<TextView
android:id="@+id/spinner_message"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="8dp"
android:text="@string/popup_loading"
android:gravity="start|center_vertical"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toEndOf="@+id/spinner_progress"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
tools:text="LOADING"
/>
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/horizontal_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<TextView
android:id="@+id/horizontal_message"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="16sp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
tools:text="LOADING DIALOG"
android:text="@string/popup_loading"
/>
<LinearLayout
android:id="@+id/progress_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="19dp"
android:orientation="horizontal"
android:weightSum="1"
app:layout_constraintTop_toBottomOf="@+id/horizontal_message"
app:layout_constraintStart_toStartOf="parent"
>
<ProgressBar
android:id="@+id/main_progress"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="match_parent"
android:layout_height="@dimen/hone_loading_progress_height"
android:layout_weight=".5"
android:progressDrawable="@drawable/hone_shape_progress"
/>
<ProgressBar
android:id="@+id/second_progress"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="match_parent"
android:layout_height="@dimen/hone_loading_progress_height"
android:layout_weight=".5"
android:visibility="gone"
android:progressDrawable="@drawable/hone_shape_progress"
/>
</LinearLayout>
<TextView
android:id="@+id/percent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
app:layout_constraintTop_toBottomOf="@+id/progress_layout"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
tools:text="0%"
android:text="0%"
tools:ignore="HardcodedText"/>
<TextView
android:id="@+id/download"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
app:layout_constraintTop_toBottomOf="@+id/progress_layout"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:text="0MB/0MB"
tools:ignore="HardcodedText"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
두번째 예제는 새롭게 로딩 다이얼로그를 생성할 경우 예제 이다.
Example
companion object {
private val mLog = org.slf4j.LoggerFactory.getLogger(CustomLoadingDialog::class.java)
val T_TRANSPARENT_LOADING = 1
}
private var mTask: TimerTask? = null
private var mTimer: Timer? = null
private var mPos = 1
override fun setLoadingDialog(params: LoadingDialog) {
when (params.type) {
T_TRANSPARENT_LOADING ->
// UI 를 변경한 로딩 다이얼로그를 호출할 수 도 있다.
transparentLoading(params)
else ->
// 기존 로딩 다이얼로그를 호출할 수 도 있고
super.setLoadingDialog(params)
}
}
////////////////////////////////////////////////////////////////////////////////////
//
// T_TRANSPARENT_LOADINGS
//
////////////////////////////////////////////////////////////////////////////////////
private fun transparentLoading(params: LoadingDialog) {
mWeakActivity.get()?.let {
val inflater = LayoutInflater.from(it)
val vmodel = ViewModelProviders.of(it as FragmentActivity)
.get(DialogLoadingViewModel::class.java)
val binding = DataBindingUtil.inflate<DetailDlgCustomLoadingLayoutBinding>(inflater,
R.layout.detail_dlg_custom_loading_layout, null, false)
binding.model = vmodel
if (!mLoadingDialog.isView) {
mLoadingDialog.setView(binding.root)
}
if (!mLoadingDialog.isShowing) {
mLoadingDialog.show()
}
mLoadingDialog.window!!.setBackgroundDrawable(
ColorDrawable(android.graphics.Color.TRANSPARENT))
mTask = object : TimerTask() {
override fun run() {
try {
if (mLog.isDebugEnabled) {
mLog.debug("EXPLODE TIMER $mPos")
}
val size = vmodel.pinList.size()
for (i in 0 until size) {
if (mPos == i) {
vmodel.pinList.get(i).set(R.drawable.hone_lockscreen_ic_dot_enable_24dp)
} else {
vmodel.pinList.get(i).set(R.drawable.ic_brightness_1_white_24dp)
}
}
if (++mPos == size) {
mPos = 0
}
} catch (ignored: Exception) {
if (mLog.isDebugEnabled) {
mLog.debug("STOP TIMER")
}
mTimer!!.cancel()
mTimer = null
}
}
}
mTimer = Timer()
mTimer!!.schedule(mTask, 1000, 500)
}
}
override fun setProgressValue(value: Long) {
if (mLoadingDialog.findViewById(R.id.other_loading_layout) == null) {
super.setProgressValue(value)
return
}
}
}
private static final org.slf4j.Logger mLog = org.slf4j.LoggerFactory.getLogger(CustomLoadingDialog.class);
public static final int T_TRANSPARENT_LOADING = 1;
public CustomLoadingDialog(Activity activity) {
super(activity);
}
@Override
protected void setLoadingDialog(@NonNull LoadingDialog params) {
switch (params.type) {
case T_TRANSPARENT_LOADING:
// UI 를 변경한 로딩 다이얼로그를 호출할 수 도 있다.
transparentLoading(params);
break;
default:
// 기존 로딩 다이얼로그를 호출할 수 도 있고
super.setLoadingDialog(params);
break;
}
}
////////////////////////////////////////////////////////////////////////////////////
//
// T_TRANSPARENT_LOADINGS
//
////////////////////////////////////////////////////////////////////////////////////
private TimerTask mTask;
private Timer mTimer;
private int mPos = 1;
private void transparentLoading(LoadingDialog params) {
LayoutInflater inflater = LayoutInflater.from(mActivity);
DialogLoadingViewModel vmodel = ViewModelProviders.of((FragmentActivity) mActivity)
.get(DialogLoadingViewModel.class);
DetailDlgCustomLoadingLayoutBinding binding = DataBindingUtil.inflate(inflater,
R.layout.detail_dlg_custom_loading_layout, null, false);
binding.setModel(vmodel);
if (!mLoadingDialog.isView()) {
mLoadingDialog.setView(binding.getRoot());
}
if (!mLoadingDialog.isShowing()) {
mLoadingDialog.show();
}
mLoadingDialog.getWindow().setBackgroundDrawable(
new ColorDrawable(android.graphics.Color.TRANSPARENT));
mTask = new TimerTask() {
@Override
public void run() {
try {
if (mLog.isDebugEnabled()) {
mLog.debug("EXPLODE TIMER " + mPos);
}
int size = vmodel.pinList.size();
for (int i = 0; i < size; ++i) {
if (mPos == i) {
vmodel.pinList.get(i).set(R.drawable.hone_lockscreen_ic_dot_enable_24dp);
} else {
vmodel.pinList.get(i).set(R.drawable.ic_brightness_1_white_24dp);
}
}
if (++mPos == size) {
mPos = 0;
}
} catch (Exception ignored) {
if (mLog.isDebugEnabled()) {
mLog.debug("STOP TIMER");
}
mTimer.cancel();
mTimer = null;
}
}
};
mTimer = new Timer();
mTimer.schedule(mTask, 1000, 500);
}
@Override
protected void setProgressValue(final long value) {
if (mLoadingDialog.findViewById(R.id.other_loading_layout) == null) {
super.setProgressValue(value);
return ;
}
}
}
XML Example
<data>
<variable
name="model"
type="com.hanwha.sample.widget.dialog.viewmodel.DialogLoadingViewModel" />
<variable
name="imageadapter"
type="com.hanwha.sample.base.bindingadapter.ImageBindingAdapter" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/other_loading_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@android:color/transparent"
android:padding="20dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/other_spinner_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/shape_custom_progress_bg"
android:padding="30dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/other_spinner_progress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/hone_lockscreen_dot_layout_margin_top"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
>
<ImageView
android:id="@+id/pin1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@{model.pin1}"
tools:ignore="ContentDescription,RtlHardcoded"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
/>
<ImageView
android:id="@+id/pin2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/hone_lockscreen_pin_margin_right"
android:src="@{model.pin2}"
tools:ignore="ContentDescription,RtlHardcoded"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintLeft_toRightOf="@+id/pin1"
/>
<ImageView
android:id="@+id/pin3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/hone_lockscreen_pin_margin_right"
android:src="@{model.pin3}"
tools:ignore="ContentDescription,RtlHardcoded"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintLeft_toRightOf="@+id/pin2"
/>
<ImageView
android:id="@+id/pin4"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/hone_lockscreen_pin_margin_right"
android:src="@{model.pin4}"
tools:ignore="ContentDescription"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintLeft_toRightOf="@+id/pin3"
/>
</androidx.constraintlayout.widget.ConstraintLayout>
<TextView
android:id="@+id/other_spinner_message"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:textSize="23sp"
android:textColor="@android:color/white"
android:text="@{model.spinnerMsg}"
android:textAppearance="@style/TextAppearance.nanum_bold"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@id/other_spinner_progress"
/>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
LoadingDialog 도 다른 다이얼로그와 마찬가지로 커스텀을 클래스를 구성한 후에는 클래스 정보를 DialogDelegate 에 설정해야 올바르게 동작 된다.
Example
DialogDelegate.get().setClassPath(DialogDelegate.DialogType.LOADING,
"$packageName.dialog.CustomLoadingDialog")
super.onCreate(savedInstanceState)
}
protected void onCreate(Bundle savedInstanceState) {
DialogDelegate.get().setClassPath(DialogDelegate.DialogType.LOADING,
getPackageName() + ".dialog.CustomLoadingDialog");
super.onCreate(savedInstanceState);
}
통신 구간 암/복호화
HONE Smart Platform에서는 통신 구간 암/복호화를 위해 IKeyProvider 와 IEncryptionProvider 인터페이스가 제공되며
이를 이용하여 3rd party 암/복호화 라이브러리를 사용할 수 있다
PreConfiguration 설정
사용자는 preConfiguration.json 파일에 네트워크 암호화에 사용 할 모든 Provider의 정보를 등록할 수 있다.
keyProviders 에는 암호화에 필요한 키를 등록할 수 있으며, encryptionProviders 에는 keyProviders 에 등록된 키를 이용하여 암호화를 하는 Provider 를 등록할 수 있다.
KeyProviders
- preConfiguration.json 의 keyProviders 안에 만들고자 하는 keyProvider 에 대한 정보를 작성한다. (IKeyProvider 를 상속)
Example
"myKeyProviderName": {
"className": "${your_class_path}.MyKeyProvider"
}
}
Example
private val key: ByteArray? = null
override fun getKey(sync: Boolean): ByteArray? {
// TODO
}
override fun setKey(key: ByteArray) {
// TODO
}
override fun isLoaded() =
true
override fun isEncrypted(sync: Boolean) =
true
}
private byte[] key;
public MyKeyProvider() {
}
@Override
public byte[] getKey(boolean sync) {
// TODO
}
@Override
public void setKey(final byte[] key) {
// TODO
}
@Override
public boolean isLoaded() {
return true;
}
@Override
public boolean isEncrypted(boolean sync) {
return true;
}
}
EncryptionProviders
- preConfiguration.json 의 encryptionProviders 안에 만들고자 하는 IEncryptionProvider 에 대한 정보를 작성한다. (IEncryptionProvider 를 상속)
Example
"myEncryptionProviderName": {
"className": "${your_class_path}.MyEncryptionProvider"
}
}
Example
override fun encrypt(key: ByteArray, planTextData: ByteArray): ByteArray? {
// TODO
}
override fun decrypt(key: ByteArray, encryptedData: ByteArray): ByteArray? {
// TODO
}
}
@Nullable
@Override
public final byte[] encrypt(@NonNull final byte[] key, @NonNull final byte[] planTextData) {
// TODO
}
@Nullable
@Override
public final byte[] decrypt(@NonNull final byte[] key, @NonNull final byte[] encryptedData) {
// TODO
}
}
Configuration 설정
configuration.json 파일에 Hub 통신시 사용 될 KeyProvider 와 EncryptionProvider 의 이름을 등록하여 사용할 수 있다. 이때 등록된 이름은 preConfiguartion.json 의 encryptionProviders, keyProviders 에 등록된 정보를 기입하면 되는데 앞선 예를 통해서 보면 KeyProvider 의 경우 myKeyProviderName 를 encryptionProvider 의 경우 myEncryptionProviderName 를 이름으로 이용하면 된다.
Example
"serviceTargets": {
"hub1": {
"baseAddress": "https://hone.hanwha.co.kr",
"port": "443",
"contextPath": "/smarthub",
"applicationPath": "/janggyo/service",
"contentType": "application/json",
"networkEncryption": {
"enabled": true,
"encryptionProviderName": "myEncryptionProviderName",
"keyProviderName": "myKeyProviderName"
}
}
}
}
BizApp 리소스 위/변조
HONE Smart Platform에서는 BizApp 리소스 위/변조 기능이 제공되며 이를 이용하기 위해서는 아래와 같은 설정 변경이 필요하다.
Actionflow 설정
Configuration 파일 중 actionflow.json에서 verifyBizAppAction을 updateBizAppAction 이후에 추가한다.
Example
{
"name": "updateBizAppAction",
"className": "honemobile.client.actionflow.action.updateBizAppAction",
"properties": {}
},
{
"name": "verifyBizAppAction",
"className": "honemobile.client.actionflow.action.verifyBizAppAction",
"properties": {}
}
]
제품 String 변경
HONE Smart Platform에서 정의한 String은 사용자가 재 지정 하여 변경할 수 있으며 이를 위해서는 제품에서 정의한 String의 키와 동일한 String의 키를 App Level에서 재정의하면 변경된다.
재 지정 파일 위치 : app/src/main/res/values/strings.xml
<!-- <string name="activity_backkey_exit">"'뒤로' 버튼을 한번 더 누르시면 종료됩니다."</string> -->
HONE Smart Platform에서 정의한 String 정보는 아래와 같다.
Table. HONE Smart Platform String
String ID | Korean | English | Comment |
---|---|---|---|
button_ok | 확인 | OK | |
button_cancel | 취소 | Cancel | |
button_confirm | 승인 | Approve | |
button_send | 보내기 | Send | |
button_clear | 지우기 | Clear | |
button_delete | 삭제 | Delete | |
button_error | 오류 | Error | |
button_notice | 알림 | Notification | |
button_alert | 경고 | Warning | |
button_exit | 종료 | Exit | |
button_update | 업데이트 | Update | |
button_retry | 재시도 | Retry | |
button_done | 완료 | Done | |
button_connect | 연결 | Connect | |
button_set | 설정 | Set | |
activity_backkey_exit | \'뒤로\'버튼을 한번 더 누르시면 종료됩니다. | Press the \’Back\’ button again to exit. | |
config_not_found_plugin | 플러그인을 찾지 못하였습니다. (%s) 이 plugin.json 내에 존재하는지 확인하세요. | Unable to fund the plug-in. Check whether (%s) exists in plugin.json. | |
config_check_error_log | Json 파일에 오류가 발생하였습니다. 오류 로그를 확인하세요. | An error has occurred in the JSON file. Check the error log. | |
config_not_found_class | 클래스가 존재하지 않습니다. (%s) | The class doesn’t exist. (%s) | |
config_invalid_networkcompress | networkCompression 활성화 시\nrequestCompress, responseCompress 값이 존재해야 합니다.\n\n예)\nrequestCompression: gzip\nresponseCompression: gzip | When activating networkCompression\nthe requestCompress and responseCompress values must exist.\n\nExample)\nrequestCompression: gzip\nresponseCompression: gzip | |
config_not_define_launcherbizapp | launcherBizAppId 가 설정되어 있지 않습니다. | launcherBizAppId is not set. | |
config_not_enable_bizappencryption | bizAppSecurity 을 활성화 하면\nbizAppEncryption 도 활성화 되어야 합니다. | To activate bizAppSecurity,\nbizAppEncryption should be also activated. | |
config_call_updatebizapp_before_requestlogin | RequestLoginAction은 UpdateBizAppAction 전에 호출되어야 합니다. | RequestLoginAction should be called before UpdateBizAppAction. | |
config_add_verifybizapp_after_updatebizapp | 위변조 활성화가 되어있을 경우 actionflow.json 에 UpdateBizAppAction 이후에 VerifyBizAppAction 추가 되어야 합니다. | If forgery and alteration are activated, VerifyBizAppAction should be added after UpdateBizAppAction in actionflow.json. | |
config_not_found_window | 윈도우를 찾을 수 없습니다. (%s)\nwindow.json을 확인하세요. | Unable to find the Window. (%s)\nCheck window.json. | |
config_no_data_in_configuration | 설정 정보 중 (%s) 값이 없습니다.\nconfiguration.json을 확인하세요. | (%s) is not set.\nCheck configuration.json. | |
config_invalid_data_in_configuration | 설정 정보 중 (%s) 값에 오류가 있습니다.\nconfiguration.json을 확인하세요. | An error has occurred in (%s).\nCheck configuration.json. | |
config_invalid_action_in_startup | resume용 Action을 startup에서 사용 하실 수 없습니다. (%s) | Unable to use (%s) Action in startup.\nCheck actionflow.json | |
config_invalid_action_in_resume | startup용 Action을 resume에서 사용 하실 수 없습니다. (%s) | Unable to use (%s) Action in resume.\nCheck actionflow.json | |
window_unknown_error | 화면 구성 중에 에러가 발생하였습니다. | An error has occurred while composing the screen. | |
webview_http_auth_error | 서버에서 사용자 인증에 실패하였습니다. | Failed to authenticate the user in the server. | |
webview_http_bad_url | 잘못된 주소입니다. | Invalid address. | |
webview_http_connect_to_server_error | 서버 연결에 실패하였습니다. | Failed to connect the server. | |
webview_http_ssl_error | SSL 수행에 실패하였습니다. | Failed to execute SSL. | |
webview_http_file_error | 파일 오류가 발생하였습니다. | A file error has occurred. | |
webview_http_file_not_found | 요청하신 파일을 찾을 수 없습니다. | Unable to find the requested file. | |
webview_http_host_lookup_error | 서버 또는 프록시 호스트 이름 조회 실패입니다. | Failed to retrieve the name of the server or proxy host. | |
webview_http_io_error | 서버에서 읽거나 서버로 쓰기 실패입니다. | Failed to read from or write to the server. | |
webview_http_proxy_auth_error | 프록시에서 사용자 인증에 실패하였습니다. | Failed to authenticate the user in the proxy. | |
webview_http_many_redirect_loop | 너무 많은 리디렉션이 발생하였습니다. | Too many redirection has occurred. | |
webview_http_timeout | 연결 시간이 초과되었습니다. | The connection time is exceeded. | |
webview_http_many_request | 페이지 로드 중 너무 많은 요청이 발생하였습니다. | To may requests have occurred while loading the page. | |
webview_unsupported_auth_scheme | 지원되지 않는 인증체계입니다. | Unsupported authentication system. | |
webview_unsupported_uri_scheme | 지원되지 않는 URI입니다. | Unsupported URI | |
webview_unknown_error | 수행 중 알 수 없는 오류가 발생하였습니다. | An unknown error has occurred during execution. | |
popup_loading | 로딩 중 | Loading | |
popup_invalid_classpath | 사용자가 설정한 다이얼로그의 클래스 경로가 올바르지 않습니다. (%s) | Invalid classpath (%s) | |
popup_window_is_popupwindow | (%s)는 팝업 윈도우입니다.\nshowPopupWindow 메소드를 이용해야 합니다. | (%s) is a popup window.\nNeed to use the showPopupWindow method. | |
popup_window_not_popupwindow | (%s)는 팝업 윈도우가 아닙니다. | (%s) isn't a popup window. | |
plugin_success | 성공 | Success | |
plugin_known_error | 플러그인 수행 중에 에러가 발생하였습니다. | An error has occurred while executing the plug-in. | |
plugin_invalid_parameter | 플러그인 파라미터가 유효하지 않습니다. | Invalid plug-in parameter. | |
plugin_invalid_action | 요청하신 플러그인 액션은 지원하지 않습니다. | The requested plug-in action is not supported. | |
plugin_invalid_service | 요청하신 플러그인 서비스는 지원하지 않습니다. | The requested plug-in service is not supported. | |
plugin_execute_error | 플러그인 실행 중 오류가 발생하였습니다. | An error has occurred while running the plug-in. | |
plugin_json_parse_error | JSON Parsing 중 에러가 발생하였습니다. | An error has occurred while parsing JSON. | |
plugin_urlencode_error | URL Encode 중 에러가 발생하였습니다. | An error has occurred while encoding the URL. | |
plugin_permission_error | 플러그인 실행 시 필요한 권한이 없습니다. | There is no right needed to execute the plug-in. | |
filerepository_file_already_exist | 해당 파일이 존재합니다. | The pertinent file already exists. | |
filerepository_unknown_address_scheme | 알 수 없는 주소 형식입니다. | Unknown address format. | |
filerepository_decompress_error | 압축을 해제하는 과정에서 오류가 발생하였습니다. | An error has occurred while decompressing. | |
filerepository_file_already_downloading | 해당 파일은 다운로드 중입니다. | The pertinent file is being downloaded now. | |
filerepository_path_not_exist | (%s)는 존재하지 않는 경로입니다. | The (%s) path doesn’t exist. | |
filerepository_not_directory | (%s)는 디렉토리가 아닙니다. | The (%s) is not a directory. | |
filerepository_file_save_error | 파일 저장 시 오류가 발생하였습니다. | An error has occurred while saving the file. | |
filerepository_filename_empty | 파일 이름이 누락되었습니다. | The file name is omitted. | |
filerepository_file_not_found | 해당 파일이 존재하지 않습니다. | The pertinent file doesn't exist. | |
securefilerepository_insert_error | 데이터베이스 추가 중 오류가 발생하였습니다. | An error has occurred while adding a database. | |
securefilerepository_write_error | 파일 쓰기 중 오류가 발생하였습니다. | A file error has occurred while writing the file. | |
securefilerepository_delete_error | 삭제 중에 오류가 발생하였습니다. | An error has occurred during deletion. | |
securefilerepository_cannot_read_file | 해당 파일을 읽을 수 없습니다. | Unable to read the requested file. | |
contact_search_error | 주소록 검색에 실패하였습니다. | Failed to search for an address book. | |
contact_insert_error | 주소록 추가에 실패하였습니다. | Failed to add an address book. | |
contact_name_is_empty | 주소록에 추가할 이름이 없습니다. | There is no name to add to the address book. | |
geolocation_bad_gps_info | 잘못된 위치 정보가 전달되었습니다. | The incorrect location information has been transferred. | |
geolocation_no_location_provider | 위치정보 지원 가능한 Provider가 없습니다. | There is no provider that can supports the location information. | |
execexternal_execute_error | 수행 중에 오류가 발생하였습니다. | An error has occurred during execution. | |
camera_not_create_image | 사진를 생성할 수 없습니다. | Unable to create a picture. | |
camera_capturing_image_error | 사진 캡쳐 중 오류가 발생하였습니다. | An error has occurred while capturing a picture. | |
camera_image_retrieve_cancelled | 사진가져오기가 취소 되었습니다. | Importing a picture has been canceled. | |
camera_not_complete | 수행이 완료되지 않았습니다. | Execution is not completed yet. | |
camera_image_not_retrieved | 사진의 경로를 검색할 수 없습니다. | Unable to search for the picture path. | |
camera_retrieving_image_error | 이미지 가져오기 중 오류가 발생하였습니다. | An error has occurred while importing an image. | |
camera_selection_cancelled | 선택이 취소되었습니다. | Selection is canceled. | |
camera_selection_not_complete | 선택이 완료되지 않았습니다. | Selection is not completed yet. | |
camera_compressing_image_error | 이미지 압축 중 오류가 발생하였습니다. | An error has occurred while compressing an image. | |
sqlite_unsupported_data_type | 지원하지 않는 자료형입니다. | Unsupported data type. | |
sqlite_not_found_db | 데이터 베이스를 찾을 수 없습니다. | Unable to find a database. | |
sqlite_delete_error | 데이터 베이스 삭제에 실패 하였습니다. | Failed to delete the database. | |
preference_key_not_found | 요청하신 Key 를 찾을 수 없습니다. | Unable to find the requested key. | |
notice_never_seen_a_day | 오늘 하루 보지 않기 | Never seen a day | |
notice_never_see_again | 다시 보지 않기 | Never see again | |
fingerprint_not_registered | 지문이 등록되어 있지 않습니다. 설정에서 지문을 추가해 주세요 | The fingerprint is not registered. Please add fingerprints in settings | |
fingerprint_too_many_attempt | 시도 횟수가 너무 많습니다. 나중에 다시 시도 하세요. | Too many attempts Please try again later. | |
fingerprint_try_again | 다시 시도 | Try again | |
fingerprint_auth_error | 인증 오류 | Authentication error | |
fingerprint_ready_atttempt | 지문을 인식시켜 주세요. | Please recognize the fingerprint. | |
gallery_title | 갤러리 | Gallery | |
gallery_all_files | 모든 파일 | All Files | |
gallery_no_storage | 저장공간이 부족하여 기능을 수행할 수 없습니다. | Unable to execute the Function due to an insufficient storage space. | iOS에서만 사용 |
securestorage_encryption_db_init_error | 암호화 DB 초기화를 실패하였습니다. | Encryption DB initialization failed. | |
lockscreen_enter_new_pincode | 새롭게 설정할 비밀번호를 입력 하세요. | Please enter a new password | |
lockscreen_enter_your_pincode | 비밀번호를 입력 하세요. | Please enter a your password | |
lockscreen_reenter_your_pincode | 비밀번호를 다시 입력 하세요. | Please re-enter your password | |
lockscreen_enter_your_old_pincode | 기존 비밀번호를 입력 하세요. | Please enter your old password. | |
screen_brightness_enable_system_write_setting | '시스템 설정 쓰기 허용'을 활성화 시켜야 올바르게 동작 합니다. | You must enable ' Allow system settings to write ' for the correct operation. | |
action_known_error | 초기화 수행 중에 에러가 발생하였습니다. | An error has occurred while executing initialization. | |
action_invalid_class | 지정하신 초기화 클래스가 존재하지 않습니다. | The designated initialization class doesn’t exist. | |
action_invalid_property | 초기화 속성이 유효하지 않습니다. | The initialization property is not valid. | |
action_execute_error | 초기화 실행 중 오류가 발생하였습니다. | An error has occurred while executing initialization. | |
action_retry_toast | 재시도 중입니다. | Re-attempting. | |
action_invalid_license | 라이센스가 유효하지 않습니다. | The license is invalid. | |
installbuiltinbizapp_loading | 필수 파일을 설치 중입니다. | Required files are being installed. | |
installbuiltinbizapp_is_empty | 설치할 필수 파일 목록이 없습니다. | There is no list of required files to install. | |
installbuiltinbizapp_no_launcherbizapp | 시작화면이 지정되지 않았습니다. | An start screen is not designated. | |
installbuiltinbizapp_invalid_launcherbizapp | 지정된 시작화면이 유효하지 않습니다. | The designated start screen is not valid. | |
installbuiltinbizapp_invalid_error | 내장된 필수 파일이 손상되었습니다. | The integrated required file is damaged. | |
installbuiltinbizapp_unformatted_error | 내장된 필수 파일의 구조는 지원하지 않습니다. | The structure of the integrated required file is not supported. | |
installbuiltinbizapp_not_found_error | 설치할 필수 파일을 찾을 수 없습니다. | Unable to find the required file to install. | |
installbuiltinbizapp_no_storage | 저장공간이 부족하여 내장된 필수 파일을 설치할 수 없습니다. | Unable to install the integrated required file due to an insufficient storage space. | |
requestlogin_loading | 단말기 정보를 등록 중입니다. | Registering the device information. | |
updateconfiguration_loading | 서버로부터 설정 정보를 가져옵니다. | Getting the configuration information from the server. | |
updatebizapp_force_update | 필수 파일의 업데이트가 있습니다. | There are updates for the required file. | |
updatebizapp_common_update | 업데이트된 파일이 있습니다. | There are updated files. | |
updatebizapp_update | 파일 업데이트 | File update | |
updatebizapp_loading | 파일 업데이트 중입니다. | Updating the file. | |
updatebizapp_complete_toast | 업데이트가 완료되었습니다. | Update is completed. | |
updatebizapp_request_info_error | 업데이트 정보 요청 중 에러가 발생하였습니다. | An error has occurred while requesting the update information. | |
updatebizapp_request_permitted_error | 업데이트 권한정보 요청 중 에러가 발생하였습니다. | An error has occurred while requesting the update right information. | |
updatebizapp_invalid_download_path | 다운로드 경로가 잘못 되었습니다. | Invalid download path. | |
updatebizapp_downloading_error | 다운로드 중 에러가 발생하였습니다. | An error has occurred while downloading. | |
updatebizapp_copy_error | 파일 복사 중 에러가 발생하였습니다. | An error has occurred while copying the file. | |
updatebizapp_decompressing_error | 압축 해제 중 에러가 발생하였습니다. | An error has occurred while decompressing. | |
updatebizapp_db_update_error | DB 갱신 중 에러가 발생하였습니다. | An error has occurred while updating the database. | |
updatebizapp_installing_error | 설치 중 에러가 발생하였습니다. | An error has occurred while installing. | |
updatebizapp_is_empty | 설치할 필수 파일 목록이 없습니다. | There is no list of required files to install. | |
updatebizapp_no_launcherbizapp | 시작화면이 지정되지 않았습니다. | An start screen is not designated. | |
updatebizapp_invalid_launcherbizapp | 지정된 시작화면이 유효하지 않습니다. | The designated start screen is not valid. | |
updatebizapp_invalid_error | 내려받은 필수 파일이 손상되었습니다. | The downloaded required file is damaged. | |
updatebizapp_unformatted_error | 내려받은 필수 파일의 구조는 지원하지 않습니다. | The structure of the downloaded required file is not supported. | |
updatebizapp_no_storage | 저장공간이 부족하여 필수 파일을 설치할 수 없습니다. | Unable to install the required file due to an insufficient storage space. | |
updatelauncher_op_force_update | 어플리케이션이 업데이트 되었습니다. | The application has been updated. | |
updatelauncher_check_version_loading | 스토어 버전 확인 중입니다. | Checking the Store version. | |
updatelauncher_common_update | 최신버전 어플리케이션이 있습니다. | The latest version application is available. | |
updatelauncher_update | 업데이트 공지 | Update notice | |
updatelauncher_store_force_update | 새로운 버전이 출시되었습니다. | A newer version has been released. | |
verifybizapp_loading | 보안검사를 실행 중입니다. | Performing security check. | |
verifybizapp_forgery_app | 설치된 파일의 변형이 의심되어 프로그램을 실행 할 수 없습니다. | Unable to run the program because the installed file could have been changed. | |
showlauncherbizapp_progress | 화면을 준비중 입니다. | Preparing the screen. | |
showlauncherbizapp_no_data | 시작화면이 지정되지 않았습니다. | An start screen is not designated. | |
showlauncherbizapp_invalid_bizapp | 필수 파일을 찾을 수 없습니다.\n 앱을 재 시작해 주세요. | Unable to find the required file.\nPlease restart. | |
checkupdate_common_update | 업데이트된 파일이 있습니다.\n 앱 종료 후 재실행 바랍니다. | There are updated files. | |
checkupdate_force_update | 필수 파일의 업데이트가 있습니다.\n 앱 종료 후 재실행 바랍니다. | There are updates for the required file. | |
server_response_error | 비정상 응답입니다. | Abnormal response. | |
server_invalid_data | 수신데이터가 유효하지 않습니다. | The receiving data is not valid. | |
server_no_launcherbizapp | 서버에 비즈앱 런처가 설정되어 있지 않습니다\nOperation Center 에 접속하여 설정하세요 | The BizApp Launcher is not set in the server.\nSet the BizApp Launcher by connecting Operation Center. | |
network_unknown_error | 네트워크 수행 중 에러가 발생하였습니다. | An error has occurred while operating the network. | |
network_net_io_error | 네트워크 통신 중 에러가 발생하였습니다. | An error has occurred while communicating over the network. | |
network_no_connectivity | 네트워크 연결이 원활하지 않습니다. | Network connection is not stable. | |
network_timeout_exceed | 네트워크 요청시간을 초과하였습니다. | The network request time is exceeded. | |
network_invalid_data_format | 지원하지 않는 데이터 포멧입니다. | Unsupported data format. | |
network_invalid_request_type | 통신상태가 원활하지 않습니다. 통신상태를 확인해주세요. | The communication state is not stable. Please check the communication state. | |
network_load_loading | 로드 중입니다. | Loading. | |
network_upload_loading | 업로드 중입니다. | Uploading. | |
network_occur_error | 네트워크에 문제가 있습니다.\n앱을 종료합니다. (%s) | There is a network problem.\nThe app will be stopped. (%s) | |
network_exit_app | 앱을 종료합니다. | The App will be stopped. | |
network_not_found_upload_file | 업로드할 대상 파일을 찾을 수 없습니다. | Unable to find the target file to upload. | |
network_not_found_file_payload | payload 내 files 를 찾을 수 없습니다. | Unable to find files inside payload. | |
network_no_storage | 저장공간이 부족하여 앱을 설치할 수 없습니다. | Unable to install the App due to an insufficient storage space. | |
network_invalid_target_address | 타겟서버의 주소를 찾을 수 없습니다. 타겟의 이름을 확인하세요. | Unable to find the target server address. Please check the name of the target. | |
network_invalid_send_data | 전송데이터가 유효하지 않습니다. | The sending data is not valid. | |
network_invalid_receive_data | 수신데이터가 유효하지 않습니다. | The receiving data is not valid. | |
network_server_comm_error | 서버통신 시 오류가 발생하였습니다. | An error has occurred in server communication. | |
network_no_honemobile_header | X-HONEMobile-Header가 없습니다. | There is no X-HONEMobile-Header. | |
network_downloading_file | 파일을 다운로드 중입니다. | Downloading a file. | |
network_ssl_cert_error | SSL 인증 오류 | SSL authentication error | |
network_ssl_cert_error_msg | 이 웹사이트에 대한 인증서가 유효하지 않습니다. | The certificate for this web site is not valid. | |
network_ssl_ignore_cert_error | 이 메시지 창을 더 이상 띄우지 않고 항상 연결 하기 | Alway connect without displaying this message window | |
exception_thread_interrupted | 스레드가 중단되었습니다. | The thread has stopped. | |
exception_execution_interrupted | 실행이 중단되었습니다. | Execution has stopped. | |
permission_title | 권한 추가 | Add a privilege | |
permission_error | 권한 오류가 발생하였습니다. | A privilege error has occurred. | |
permission_message | 앱을 올바르게 실행하기 위해서는 권한 추가가 필요 합니다. | You need to add a privilege to run the App properly. | |