refactor the Android TV-App prompt message (#33469)
* refactor the class MatterCommissioningPrompter.java,don't need Activity no more to avoid memory leak;
* Restyled by google-java-format
---------
Co-authored-by: Restyled.io <commits@restyled.io>
diff --git a/examples/tv-app/android/App/platform-app/build.gradle b/examples/tv-app/android/App/platform-app/build.gradle
index 967d697..0845105 100644
--- a/examples/tv-app/android/App/platform-app/build.gradle
+++ b/examples/tv-app/android/App/platform-app/build.gradle
@@ -7,7 +7,7 @@
defaultConfig {
applicationId "com.matter.tv.server"
- minSdk 24
+ minSdk 26
targetSdk 30
versionCode 1
versionName "1.0"
@@ -57,6 +57,9 @@
// ]
}
}
+ buildFeatures {
+ viewBinding = true
+ }
}
dependencies {
diff --git a/examples/tv-app/android/App/platform-app/src/main/AndroidManifest.xml b/examples/tv-app/android/App/platform-app/src/main/AndroidManifest.xml
index 04b30f2..b6e00cd 100644
--- a/examples/tv-app/android/App/platform-app/src/main/AndroidManifest.xml
+++ b/examples/tv-app/android/App/platform-app/src/main/AndroidManifest.xml
@@ -24,6 +24,7 @@
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"
tools:ignore="QueryAllPackagesPermission" />
+ <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<application
android:allowBackup="true"
@@ -36,6 +37,7 @@
android:theme="@style/Theme.MatterTVSlave">
<activity android:name="com.matter.tv.server.MainActivity"
+ android:exported="true"
android:launchMode="singleTask">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
diff --git a/examples/tv-app/android/App/platform-app/src/main/java/com/matter/tv/server/MainActivity.java b/examples/tv-app/android/App/platform-app/src/main/java/com/matter/tv/server/MainActivity.java
index 2fb375d..df612cf 100644
--- a/examples/tv-app/android/App/platform-app/src/main/java/com/matter/tv/server/MainActivity.java
+++ b/examples/tv-app/android/App/platform-app/src/main/java/com/matter/tv/server/MainActivity.java
@@ -1,13 +1,16 @@
package com.matter.tv.server;
+import android.content.Intent;
+import android.net.Uri;
import android.os.Bundle;
+import android.provider.Settings;
+import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.fragment.app.Fragment;
import com.google.android.material.bottomnavigation.BottomNavigationView;
import com.matter.tv.server.fragments.ContentAppFragment;
import com.matter.tv.server.fragments.QrCodeFragment;
import com.matter.tv.server.fragments.TerminalFragment;
-import com.matter.tv.server.service.MatterServant;
import java.util.LinkedHashMap;
public class MainActivity extends AppCompatActivity {
@@ -41,10 +44,6 @@
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
- // MainActivity is needed to launch dialog prompt
- // in UserPrompter
- MatterServant.get().setActivity(this);
-
BottomNavigationView bottomNavigationView = findViewById(R.id.bottom_navigation);
bottomNavigationView.setOnItemSelectedListener(navListener);
@@ -52,5 +51,25 @@
.beginTransaction()
.replace(R.id.fragment_container_view, new QrCodeFragment())
.commit();
+ checkOverlayPermission();
+ }
+
+ private void checkOverlayPermission() {
+ if (!Settings.canDrawOverlays(this)) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder
+ .setMessage("Allow permission to display over other apps")
+ .setTitle("Request overlay permission")
+ .setPositiveButton(
+ "Ok",
+ (dialog, which) -> {
+ dialog.dismiss();
+ Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION);
+ intent.setData(Uri.parse("package:" + getPackageName()));
+ startActivity(intent);
+ })
+ .create()
+ .show();
+ }
}
}
diff --git a/examples/tv-app/android/App/platform-app/src/main/java/com/matter/tv/server/MatterCommissioningPrompter.java b/examples/tv-app/android/App/platform-app/src/main/java/com/matter/tv/server/MatterCommissioningPrompter.java
old mode 100644
new mode 100755
index 8d2a650..743db56
--- a/examples/tv-app/android/App/platform-app/src/main/java/com/matter/tv/server/MatterCommissioningPrompter.java
+++ b/examples/tv-app/android/App/platform-app/src/main/java/com/matter/tv/server/MatterCommissioningPrompter.java
@@ -2,21 +2,31 @@
import static androidx.core.content.ContextCompat.getSystemService;
-import android.app.Activity;
+import android.annotation.SuppressLint;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.content.ContentResolver;
import android.content.Context;
-import android.os.Build;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
import android.provider.Settings;
import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.WindowManager;
import android.widget.EditText;
+import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.core.app.NotificationCompat;
-import com.matter.tv.server.service.MatterServant;
+import com.matter.tv.server.databinding.LayoutDialogBinding;
+import com.matter.tv.server.databinding.LayoutInputDialogBinding;
+import com.matter.tv.server.model.PromptCommissionerPasscode;
import com.matter.tv.server.tvapp.Message;
import com.matter.tv.server.tvapp.UserPrompter;
import com.matter.tv.server.tvapp.UserPrompterResolver;
+import com.matter.tv.server.utils.PxConvert;
+import java.lang.ref.WeakReference;
public class MatterCommissioningPrompter extends UserPrompterResolver implements UserPrompter {
@@ -26,16 +36,14 @@
private final int SUCCESS_ID = 0;
private final int FAIL_ID = 1;
+ private final MsgHandler mHandler = new MsgHandler(this);
+
public MatterCommissioningPrompter(Context context) {
this.context = context;
this.createNotificationChannel();
setUserPrompter(this);
}
- private Activity getActivity() {
- return MatterServant.get().getActivity();
- }
-
public void promptForCommissionOkPermission(
int vendorId, int productId, String commissioneeName) {
Log.d(
@@ -46,7 +54,6 @@
+ productId
+ ". Commissionee: "
+ commissioneeName);
-
ContentResolver contentResolver = context.getContentResolver();
boolean authorisationDialogDisabled =
Settings.Secure.getInt(contentResolver, "matter_show_authorisation_dialog", 0) == 0;
@@ -57,29 +64,10 @@
OnPromptAccepted();
return;
}
-
- getActivity()
- .runOnUiThread(
- () -> {
- AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
- builder
- .setMessage(
- commissioneeName
- + " is requesting permission to cast to this device, approve?")
- .setTitle("Allow access to " + commissioneeName)
- .setPositiveButton(
- "Ok",
- (dialog, which) -> {
- OnPromptAccepted();
- })
- .setNegativeButton(
- "Cancel",
- (dialog, which) -> {
- OnPromptDeclined();
- })
- .create()
- .show();
- });
+ android.os.Message obtained = android.os.Message.obtain();
+ obtained.what = MsgHandler.MSG_CommissionOkPermission;
+ obtained.obj = commissioneeName;
+ mHandler.sendMessage(obtained);
}
@Override
@@ -92,31 +80,10 @@
+ productId
+ ". Commissionee: "
+ commissioneeName);
-
- getActivity()
- .runOnUiThread(
- () -> {
- EditText editText = new EditText(getActivity());
- AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
-
- builder
- .setMessage("Please enter PIN displayed in casting app.")
- .setTitle("Allow access to " + commissioneeName)
- .setView(editText)
- .setPositiveButton(
- "Ok",
- (dialog, which) -> {
- String pinCode = editText.getText().toString();
- OnPinCodeEntered(Integer.parseInt(pinCode));
- })
- .setNegativeButton(
- "Cancel",
- (dialog, which) -> {
- OnPinCodeDeclined();
- })
- .create()
- .show();
- });
+ android.os.Message obtained = android.os.Message.obtain();
+ obtained.what = MsgHandler.MSG_CommissionPinCode;
+ obtained.obj = commissioneeName;
+ mHandler.sendMessage(obtained);
}
public void hidePromptsOnCancel(int vendorId, int productId, String commissioneeName) {
@@ -128,26 +95,10 @@
+ productId
+ ". Commissionee: "
+ commissioneeName);
-
- getActivity()
- .runOnUiThread(
- () -> {
- AlertDialog.Builder abuilder = new AlertDialog.Builder(getActivity());
- abuilder
- .setMessage("Cancelled connection to " + commissioneeName)
- .setTitle("Connection Cancelled")
- .create()
- .show();
-
- NotificationCompat.Builder builder =
- new NotificationCompat.Builder(getActivity(), CHANNEL_ID)
- .setSmallIcon(R.drawable.ic_baseline_check_24)
- .setContentTitle("Connection Cancelled")
- .setContentText("Cancelled connection to " + commissioneeName)
- .setPriority(NotificationCompat.PRIORITY_DEFAULT);
-
- notificationManager.notify(SUCCESS_ID, builder.build());
- });
+ android.os.Message obtained = android.os.Message.obtain();
+ obtained.what = MsgHandler.MSG_HidePromptsOnCancel;
+ obtained.obj = commissioneeName;
+ mHandler.sendMessage(obtained);
}
public void promptWithCommissionerPasscode(
@@ -167,38 +118,15 @@
+ productId
+ ". Commissionee: "
+ commissioneeName);
-
- getActivity()
- .runOnUiThread(
- () -> {
- EditText editText = new EditText(getActivity());
- AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
-
- builder
- .setMessage(
- "Please enter "
- + passcode
- + " in "
- + commissioneeName
- + " app. "
- + pairingInstruction
- + " ["
- + pairingHint
- + "]")
- .setTitle("Passcode" + passcode)
- .setPositiveButton(
- "Ok",
- (dialog, which) -> {
- OnCommissionerPasscodeOK();
- })
- .setNegativeButton(
- "Cancel",
- (dialog, which) -> {
- OnCommissionerPasscodeCancel();
- })
- .create()
- .show();
- });
+ Bundle bundle = new Bundle();
+ PromptCommissionerPasscode promptCommissionerPasscode =
+ new PromptCommissionerPasscode(
+ vendorId, productId, commissioneeName, passcode, pairingHint, pairingInstruction);
+ bundle.putParcelable(MsgHandler.KEY_PROMPT_COMMISSIONER_PASSCODE, promptCommissionerPasscode);
+ android.os.Message obtained = android.os.Message.obtain();
+ obtained.what = MsgHandler.MSG_CommissionerPasscode;
+ obtained.setData(bundle);
+ mHandler.sendMessage(obtained);
}
public void promptCommissioningStarted(int vendorId, int productId, String commissioneeName) {
@@ -210,25 +138,10 @@
+ productId
+ ". Commissionee: "
+ commissioneeName);
- getActivity()
- .runOnUiThread(
- () -> {
- AlertDialog.Builder abuilder = new AlertDialog.Builder(getActivity());
- abuilder
- .setMessage("Starting connection to " + commissioneeName)
- .setTitle("Connection Starting")
- .create()
- .show();
-
- NotificationCompat.Builder builder =
- new NotificationCompat.Builder(getActivity(), CHANNEL_ID)
- .setSmallIcon(R.drawable.ic_baseline_check_24)
- .setContentTitle("Connection Starting")
- .setContentText("Starting connection to " + commissioneeName)
- .setPriority(NotificationCompat.PRIORITY_DEFAULT);
-
- notificationManager.notify(SUCCESS_ID, builder.build());
- });
+ android.os.Message obtained = android.os.Message.obtain();
+ obtained.what = MsgHandler.MSG_CommissioningStarted;
+ obtained.obj = commissioneeName;
+ mHandler.sendMessage(obtained);
}
public void promptCommissioningSucceeded(int vendorId, int productId, String commissioneeName) {
@@ -240,116 +153,360 @@
+ productId
+ ". Commissionee: "
+ commissioneeName);
- getActivity()
- .runOnUiThread(
- () -> {
- AlertDialog.Builder abuilder = new AlertDialog.Builder(getActivity());
- abuilder
- .setMessage(
- "Success. "
- + commissioneeName
- + " can now cast to this device. Visit settings to manage access control for casting.")
- .setTitle("Connection Complete")
- .create()
- .show();
-
- NotificationCompat.Builder builder =
- new NotificationCompat.Builder(getActivity(), CHANNEL_ID)
- .setSmallIcon(R.drawable.ic_baseline_check_24)
- .setContentTitle("Connection Complete")
- .setContentText(
- "Success. "
- + commissioneeName
- + " can now cast to this device. Visit settings to manage access control for casting.")
- .setPriority(NotificationCompat.PRIORITY_DEFAULT);
-
- notificationManager.notify(SUCCESS_ID, builder.build());
- });
+ android.os.Message obtained = android.os.Message.obtain();
+ obtained.what = MsgHandler.MSG_CommissioningSucceeded;
+ obtained.obj = commissioneeName;
+ mHandler.sendMessage(obtained);
}
public void promptCommissioningFailed(String commissioneeName, String error) {
Log.d(TAG, "Received prompt for failure Commissionee: " + commissioneeName);
- getActivity()
- .runOnUiThread(
- () -> {
- AlertDialog.Builder abuilder = new AlertDialog.Builder(getActivity());
- abuilder
- .setMessage("Failed. " + commissioneeName + " experienced error: " + error + ".")
- .setTitle("Connection Failed")
- .create()
- .show();
- NotificationCompat.Builder builder =
- new NotificationCompat.Builder(getActivity(), CHANNEL_ID)
- .setSmallIcon(R.drawable.ic_baseline_clear_24)
- .setContentTitle("Connection Failed")
- .setContentText(
- "Failed. " + commissioneeName + " experienced error: " + error + ".")
- .setPriority(NotificationCompat.PRIORITY_DEFAULT);
-
- notificationManager.notify(FAIL_ID, builder.build());
- });
+ android.os.Message obtained = android.os.Message.obtain();
+ obtained.what = MsgHandler.MSG_CommissioningFailed;
+ obtained.obj = commissioneeName;
+ mHandler.sendMessage(obtained);
}
public void promptWithMessage(Message message) {
Log.d(TAG, "Received message prompt for " + message.messageText);
- getActivity()
- .runOnUiThread(
- () -> {
- if (message.responseOptions.length != 2) {
- AlertDialog.Builder abuilder = new AlertDialog.Builder(getActivity());
- abuilder
- .setMessage("" + message.messageId + ":" + message.messageText)
- .setTitle("New Message from Test")
- .setPositiveButton(
- "Ok",
- (dialog, which) -> {
- OnMessageResponse(message.messageId, 0); // ack
- })
- .setNegativeButton(
- "Ignore",
- (dialog, which) -> {
- OnMessageResponse(message.messageId, -1); // ignore
- })
- .create()
- .show();
- } else {
- AlertDialog.Builder abuilder = new AlertDialog.Builder(getActivity());
- abuilder
- .setMessage("" + message.messageId + ":" + message.messageText)
- .setTitle("New Message from Test")
- .setPositiveButton(
- message.responseOptions[0].label,
- (dialog, which) -> {
- OnMessageResponse(message.messageId, message.responseOptions[0].id);
- })
- .setNegativeButton(
- message.responseOptions[1].label,
- (dialog, which) -> {
- OnMessageResponse(message.messageId, message.responseOptions[1].id);
- })
- .create()
- .show();
- }
- });
+
+ android.os.Message obtained = android.os.Message.obtain();
+ obtained.what = MsgHandler.MSG_PromptWithMessage;
+ obtained.obj = message;
+ mHandler.sendMessage(obtained);
+ }
+
+ @SuppressLint("ClickableViewAccessibility")
+ private void handleCommissionOkPermission(android.os.Message msg) {
+ String commissioneeName = (String) msg.obj;
+
+ PromptDialogManager promptDialogManager =
+ new PromptDialogManager.Builder()
+ .setCancelable(false)
+ .setWidth(PxConvert.dp2px(MatterTvServerApplication.getApplication(), 350))
+ .setStyle(PromptDialogManager.DIALOG_STYLE_1)
+ .build();
+
+ LayoutDialogBinding udcDialogBinding =
+ LayoutDialogBinding.inflate(
+ LayoutInflater.from(
+ MatterTvServerApplication.getApplication().getApplicationContext()));
+ udcDialogBinding.tvContent.setText(
+ commissioneeName + " is requesting permission to cast to this device, approve?");
+ udcDialogBinding.tvTitle.setText("Allow access to " + commissioneeName);
+ udcDialogBinding.tvLeft.setOnClickListener(
+ v -> {
+ OnPromptDeclined();
+ promptDialogManager.removeViewImmediate(udcDialogBinding.getRoot());
+ });
+ udcDialogBinding.tvRight.setOnClickListener(
+ v -> {
+ OnPromptAccepted();
+ promptDialogManager.removeViewImmediate(udcDialogBinding.getRoot());
+ });
+ promptDialogManager.addView(udcDialogBinding.getRoot(), this::OnPromptDeclined);
+ }
+
+ private void handleCommissionPinCode(android.os.Message msg) {
+ String commissioneeName = (String) msg.obj;
+
+ PromptDialogManager promptDialogManager =
+ new PromptDialogManager.Builder()
+ .setCancelable(false)
+ .setWidth(PxConvert.dp2px(MatterTvServerApplication.getApplication(), 350))
+ .setStyle(PromptDialogManager.DIALOG_STYLE_2)
+ .build();
+
+ LayoutInputDialogBinding inputDialogBinding =
+ LayoutInputDialogBinding.inflate(
+ LayoutInflater.from(
+ MatterTvServerApplication.getApplication().getApplicationContext()));
+ EditText etPin = inputDialogBinding.etPin;
+ inputDialogBinding.tvTitle.setText("Allow access to " + commissioneeName);
+ inputDialogBinding.tvContent.setText("Please enter PIN displayed in casting app.");
+ inputDialogBinding.tvLeft.setOnClickListener(
+ view -> {
+ OnPinCodeDeclined();
+ promptDialogManager.removeViewImmediate(inputDialogBinding.getRoot());
+ });
+ inputDialogBinding.tvRight.setOnClickListener(
+ view -> {
+ String pinCode = etPin.getText().toString();
+ OnPinCodeEntered(Integer.parseInt(pinCode));
+ promptDialogManager.removeViewImmediate(inputDialogBinding.getRoot());
+ });
+ promptDialogManager.addView(inputDialogBinding.getRoot(), this::OnPinCodeDeclined);
+ }
+
+ private void handleHidePromptsOnCancel(android.os.Message msg) {
+ String commissioneeName = (String) msg.obj;
+
+ PromptDialogManager promptDialogManager =
+ new PromptDialogManager.Builder()
+ .setCancelable(true)
+ .setWidth(PxConvert.dp2px(MatterTvServerApplication.getApplication(), 300))
+ .setStyle(PromptDialogManager.DIALOG_STYLE_3)
+ .build();
+
+ LayoutDialogBinding dialogBinding =
+ LayoutDialogBinding.inflate(
+ LayoutInflater.from(
+ MatterTvServerApplication.getApplication().getApplicationContext()));
+ dialogBinding.tvContent.setText("Cancelled connection to " + commissioneeName);
+ dialogBinding.tvTitle.setText("Connection Cancelled");
+ dialogBinding.llAction.setVisibility(View.GONE);
+ promptDialogManager.addView(dialogBinding.getRoot(), null);
+
+ NotificationCompat.Builder builder =
+ new NotificationCompat.Builder(getContext(), CHANNEL_ID)
+ .setSmallIcon(R.drawable.ic_baseline_check_24)
+ .setContentTitle("Connection Cancelled")
+ .setContentText("Cancelled connection to " + commissioneeName)
+ .setPriority(NotificationCompat.PRIORITY_DEFAULT);
+
+ notificationManager.notify(SUCCESS_ID, builder.build());
+ }
+
+ private void handleCommissionerPasscode(android.os.Message msg) {
+ Bundle bundle = msg.getData();
+ PromptCommissionerPasscode parcelable =
+ bundle.getParcelable(MsgHandler.KEY_PROMPT_COMMISSIONER_PASSCODE);
+
+ PromptDialogManager promptDialogManager =
+ new PromptDialogManager.Builder()
+ .setCancelable(false)
+ .setWidth(PxConvert.dp2px(MatterTvServerApplication.getApplication(), 350))
+ .setStyle(PromptDialogManager.DIALOG_STYLE_1)
+ .build();
+
+ LayoutDialogBinding udcDialogBinding =
+ LayoutDialogBinding.inflate(
+ LayoutInflater.from(
+ MatterTvServerApplication.getApplication().getApplicationContext()));
+ udcDialogBinding.tvContent.setText(
+ "Please enter "
+ + parcelable.getPasscode()
+ + " in "
+ + parcelable.getCommissioneeName()
+ + " app. "
+ + parcelable.getPairingInstruction()
+ + " ["
+ + parcelable.getPairingHint()
+ + "]");
+ udcDialogBinding.tvTitle.setText("Passcode" + parcelable.getPasscode());
+ udcDialogBinding.tvLeft.setOnClickListener(
+ v -> {
+ OnCommissionerPasscodeCancel();
+ promptDialogManager.removeViewImmediate(udcDialogBinding.getRoot());
+ });
+ udcDialogBinding.tvRight.setOnClickListener(
+ v -> {
+ OnCommissionerPasscodeOK();
+ promptDialogManager.removeViewImmediate(udcDialogBinding.getRoot());
+ });
+ promptDialogManager.addView(udcDialogBinding.getRoot(), this::OnCommissionerPasscodeCancel);
+ }
+
+ private void handleCommissioningStarted(android.os.Message msg) {
+ String commissioneeName = (String) msg.obj;
+
+ PromptDialogManager promptDialogManager =
+ new PromptDialogManager.Builder()
+ .setCancelable(true)
+ .setWidth(PxConvert.dp2px(MatterTvServerApplication.getApplication(), 300))
+ .setStyle(PromptDialogManager.DIALOG_STYLE_3)
+ .build();
+
+ LayoutDialogBinding dialogBinding =
+ LayoutDialogBinding.inflate(
+ LayoutInflater.from(
+ MatterTvServerApplication.getApplication().getApplicationContext()));
+ dialogBinding.tvContent.setText("Starting connection to " + commissioneeName);
+ dialogBinding.tvTitle.setText("Connection Starting");
+ dialogBinding.llAction.setVisibility(View.GONE);
+ promptDialogManager.addView(dialogBinding.getRoot(), null);
+
+ NotificationCompat.Builder builder =
+ new NotificationCompat.Builder(getContext(), CHANNEL_ID)
+ .setSmallIcon(R.drawable.ic_baseline_check_24)
+ .setContentTitle("Connection Starting")
+ .setContentText("Starting connection to " + commissioneeName)
+ .setPriority(NotificationCompat.PRIORITY_DEFAULT);
+
+ notificationManager.notify(SUCCESS_ID, builder.build());
+ }
+
+ private void handleCommissioningSucceeded(android.os.Message msg) {
+ String commissioneeName = (String) msg.obj;
+ PromptDialogManager promptDialogManager =
+ new PromptDialogManager.Builder()
+ .setCancelable(true)
+ .setWidth(PxConvert.dp2px(MatterTvServerApplication.getApplication(), 300))
+ .setStyle(PromptDialogManager.DIALOG_STYLE_3)
+ .build();
+
+ LayoutDialogBinding dialogBinding =
+ LayoutDialogBinding.inflate(
+ LayoutInflater.from(
+ MatterTvServerApplication.getApplication().getApplicationContext()));
+ dialogBinding.tvContent.setText(
+ "Success. "
+ + commissioneeName
+ + " can now cast to this device. Visit settings to manage access control for casting.");
+ dialogBinding.tvTitle.setText("Connection Complete");
+ dialogBinding.llAction.setVisibility(View.GONE);
+ promptDialogManager.addView(dialogBinding.getRoot(), null);
+
+ NotificationCompat.Builder builder =
+ new NotificationCompat.Builder(getContext(), CHANNEL_ID)
+ .setSmallIcon(R.drawable.ic_baseline_check_24)
+ .setContentTitle("Connection Complete")
+ .setContentText(
+ "Success. "
+ + commissioneeName
+ + " can now cast to this device. Visit settings to manage access control for casting.")
+ .setPriority(NotificationCompat.PRIORITY_DEFAULT);
+
+ notificationManager.notify(SUCCESS_ID, builder.build());
+ }
+
+ private void handleCommissioningFailed(android.os.Message msg) {
+ String commissioneeName = (String) msg.obj;
+ String error = (String) msg.obj;
+ AlertDialog.Builder abuilder = new AlertDialog.Builder(getContext());
+ AlertDialog alertDialog =
+ abuilder
+ .setMessage("Failed. " + commissioneeName + " experienced error: " + error + ".")
+ .setTitle("Connection Failed")
+ .create();
+ alertDialog.getWindow().setType(WindowManager.LayoutParams.TYPE_APPLICATION_PANEL);
+ alertDialog.show();
+
+ NotificationCompat.Builder builder =
+ new NotificationCompat.Builder(getContext(), CHANNEL_ID)
+ .setSmallIcon(R.drawable.ic_baseline_clear_24)
+ .setContentTitle("Connection Failed")
+ .setContentText("Failed. " + commissioneeName + " experienced error: " + error + ".")
+ .setPriority(NotificationCompat.PRIORITY_DEFAULT);
+
+ notificationManager.notify(FAIL_ID, builder.build());
+ }
+
+ private void handlePromptWithMessage(android.os.Message msg) {
+ Message message = (Message) msg.obj;
+
+ PromptDialogManager promptDialogManager =
+ new PromptDialogManager.Builder()
+ .setCancelable(false)
+ .setWidth(PxConvert.dp2px(MatterTvServerApplication.getApplication(), 380))
+ .setStyle(PromptDialogManager.DIALOG_STYLE_1)
+ .build();
+
+ LayoutDialogBinding dialogBinding =
+ LayoutDialogBinding.inflate(
+ LayoutInflater.from(
+ MatterTvServerApplication.getApplication().getApplicationContext()));
+ dialogBinding.tvContent.setText("" + message.messageId + ":" + message.messageText);
+ dialogBinding.tvTitle.setText("New Message from Test");
+ if (message.responseOptions.length != 2) {
+ dialogBinding.tvLeft.setText("Ignore");
+ dialogBinding.tvRight.setText("Ok");
+ dialogBinding.tvLeft.setOnClickListener(
+ v -> {
+ OnMessageResponse(message.messageId, -1);
+ promptDialogManager.removeViewImmediate(dialogBinding.getRoot());
+ });
+ dialogBinding.tvRight.setOnClickListener(
+ v -> {
+ OnMessageResponse(message.messageId, 0);
+ promptDialogManager.removeViewImmediate(dialogBinding.getRoot());
+ });
+ } else {
+ dialogBinding.tvLeft.setText(message.responseOptions[1].label);
+ dialogBinding.tvRight.setText(message.responseOptions[0].label);
+ dialogBinding.tvLeft.setOnClickListener(
+ v -> {
+ OnMessageResponse(message.messageId, message.responseOptions[1].id);
+ promptDialogManager.removeViewImmediate(dialogBinding.getRoot());
+ });
+ dialogBinding.tvRight.setOnClickListener(
+ v -> {
+ OnMessageResponse(message.messageId, message.responseOptions[0].id);
+ promptDialogManager.removeViewImmediate(dialogBinding.getRoot());
+ });
+ }
+ promptDialogManager.addView(
+ dialogBinding.getRoot(),
+ () -> {
+ if (message.responseOptions.length != 2) {
+ OnMessageResponse(message.messageId, -1);
+ promptDialogManager.removeViewImmediate(dialogBinding.getRoot());
+ } else {
+ OnMessageResponse(message.messageId, message.responseOptions[1].id);
+ promptDialogManager.removeViewImmediate(dialogBinding.getRoot());
+ }
+ });
}
private void createNotificationChannel() {
- // Create the NotificationChannel, but only on API 26+ because
- // the NotificationChannel class is new and not in the support library
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
- Log.d(TAG, " ------------- createNotificationChannel");
- CharSequence name = "MatterPromptNotificationChannel";
- String description = "Matter Channel for sending notifications";
- int importance = NotificationManager.IMPORTANCE_DEFAULT;
- NotificationChannel channel = new NotificationChannel(CHANNEL_ID, name, importance);
- channel.setDescription(description);
- // Register the channel with the system; you can't change the importance
- // or other notification behaviors after this
- this.notificationManager = getSystemService(context, NotificationManager.class);
- notificationManager.createNotificationChannel(channel);
- } else {
- Log.d(TAG, " ------------- NOT createNotificationChannel");
+ Log.d(TAG, " ------------- createNotificationChannel");
+ CharSequence name = "MatterPromptNotificationChannel";
+ String description = "Matter Channel for sending notifications";
+ int importance = NotificationManager.IMPORTANCE_DEFAULT;
+ NotificationChannel channel = new NotificationChannel(CHANNEL_ID, name, importance);
+ channel.setDescription(description);
+ // Register the channel with the system; you can't change the importance
+ // or other notification behaviors after this
+ this.notificationManager = getSystemService(context, NotificationManager.class);
+ notificationManager.createNotificationChannel(channel);
+ }
+
+ private Context getContext() {
+ return MatterTvServerApplication.getApplication();
+ }
+
+ static class MsgHandler extends Handler {
+ public static final int MSG_CommissionOkPermission = 101;
+ public static final int MSG_CommissionPinCode = 102;
+ public static final int MSG_HidePromptsOnCancel = 103;
+ public static final int MSG_CommissionerPasscode = 104;
+ public static final int MSG_CommissioningStarted = 105;
+ public static final int MSG_CommissioningSucceeded = 106;
+ public static final int MSG_CommissioningFailed = 107;
+ public static final int MSG_PromptWithMessage = 108;
+ public static final String KEY_PROMPT_COMMISSIONER_PASSCODE =
+ "KEY_PROMPT_COMMISSIONER_PASSCODE";
+ private final WeakReference<MatterCommissioningPrompter> mPrompterWeakReference;
+
+ public MsgHandler(MatterCommissioningPrompter commissioningPrompter) {
+ super(Looper.getMainLooper());
+ mPrompterWeakReference = new WeakReference<>(commissioningPrompter);
+ }
+
+ @Override
+ public void handleMessage(@NonNull android.os.Message msg) {
+ MatterCommissioningPrompter commissioningPrompter = mPrompterWeakReference.get();
+ if (commissioningPrompter == null) {
+ return;
+ }
+ if (msg.what == MSG_CommissionOkPermission) {
+ commissioningPrompter.handleCommissionOkPermission(msg);
+ } else if (msg.what == MSG_CommissionPinCode) {
+ commissioningPrompter.handleCommissionPinCode(msg);
+ } else if (msg.what == MSG_HidePromptsOnCancel) {
+ commissioningPrompter.handleHidePromptsOnCancel(msg);
+ } else if (msg.what == MSG_CommissionerPasscode) {
+ commissioningPrompter.handleCommissionerPasscode(msg);
+ } else if (msg.what == MSG_CommissioningStarted) {
+ commissioningPrompter.handleCommissioningStarted(msg);
+ } else if (msg.what == MSG_CommissioningSucceeded) {
+ commissioningPrompter.handleCommissioningSucceeded(msg);
+ } else if (msg.what == MSG_CommissioningFailed) {
+ commissioningPrompter.handleCommissioningFailed(msg);
+ } else if (msg.what == MSG_PromptWithMessage) {
+ commissioningPrompter.handlePromptWithMessage(msg);
+ }
}
}
}
diff --git a/examples/tv-app/android/App/platform-app/src/main/java/com/matter/tv/server/MatterTvServerApplication.java b/examples/tv-app/android/App/platform-app/src/main/java/com/matter/tv/server/MatterTvServerApplication.java
index c2f1579..9da6f3f 100644
--- a/examples/tv-app/android/App/platform-app/src/main/java/com/matter/tv/server/MatterTvServerApplication.java
+++ b/examples/tv-app/android/App/platform-app/src/main/java/com/matter/tv/server/MatterTvServerApplication.java
@@ -6,9 +6,16 @@
import com.matter.tv.server.service.MatterServantService;
public class MatterTvServerApplication extends Application {
+ private static Application mApp;
+
+ public static Application getApplication() {
+ return mApp;
+ }
+
@Override
public void onCreate() {
super.onCreate();
+ mApp = this;
startMatterServantService();
}
diff --git a/examples/tv-app/android/App/platform-app/src/main/java/com/matter/tv/server/PromptDialogManager.java b/examples/tv-app/android/App/platform-app/src/main/java/com/matter/tv/server/PromptDialogManager.java
new file mode 100644
index 0000000..3548de3
--- /dev/null
+++ b/examples/tv-app/android/App/platform-app/src/main/java/com/matter/tv/server/PromptDialogManager.java
@@ -0,0 +1,170 @@
+package com.matter.tv.server;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.graphics.PixelFormat;
+import android.util.Log;
+import android.view.Gravity;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.WindowManager;
+import androidx.annotation.Nullable;
+
+public class PromptDialogManager {
+ private static final String TAG = "PromptDialogManager";
+ /** normal type,like a standard AlertDialog */
+ public static final int DIALOG_STYLE_1 = 1;
+ /** input type,include an EditText */
+ public static final int DIALOG_STYLE_2 = 2;
+ /** normal type,no action area */
+ public static final int DIALOG_STYLE_3 = 3;
+
+ public static final int DIALOG_STYLE_4 = 4;
+ private final int width;
+ private final int height;
+ public final float dimAmount;
+ public final int style;
+ private final boolean cancelable;
+
+ private PromptDialogManager(Builder builder) {
+ this.cancelable = builder.cancelable;
+ this.height = builder.height;
+ this.width = builder.width;
+ this.dimAmount = builder.dimAmount;
+ this.style = builder.style;
+ }
+
+ @SuppressLint("ClickableViewAccessibility")
+ public void addView(View view, @Nullable onBackPressListener onBackPressListener) {
+ WindowManager.LayoutParams params = new WindowManager.LayoutParams();
+ params.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
+ params.format = PixelFormat.TRANSLUCENT;
+ params.gravity = Gravity.CENTER;
+ params.width = this.width;
+ params.height = this.height;
+ params.windowAnimations = android.R.style.Animation_Dialog;
+ params.dimAmount = this.dimAmount;
+ params.flags = convert2Flag(this.style);
+ WindowManager windowManager =
+ (WindowManager)
+ MatterTvServerApplication.getApplication()
+ .getApplicationContext()
+ .getSystemService(Context.WINDOW_SERVICE);
+
+ view.setFocusable(true);
+ view.setFocusableInTouchMode(true);
+ view.requestFocus();
+
+ View.OnKeyListener onKeyListener =
+ (view1, keyCode, keyEvent) -> {
+ Log.d(TAG, "addView: setOnKeyListener keyCode = " + keyCode);
+ if (keyCode == KeyEvent.KEYCODE_BACK && keyEvent.getAction() == KeyEvent.ACTION_UP) {
+ windowManager.removeView(view);
+ if (onBackPressListener != null) {
+ onBackPressListener.onBackPress();
+ }
+ return true;
+ }
+ return false;
+ };
+ view.setOnKeyListener(onKeyListener);
+ view.setOnTouchListener(
+ (v, motionEvent) -> {
+ if (motionEvent.getAction() == MotionEvent.ACTION_OUTSIDE) {
+ Log.d(TAG, "handleCommissionOkPermission: setOnTouchListener ACTION_OUTSIDE");
+ windowManager.removeView(view);
+ return true;
+ }
+ return false;
+ });
+
+ windowManager.addView(view, params);
+ }
+
+ public void removeView(View view) {
+ WindowManager windowManager =
+ (WindowManager)
+ MatterTvServerApplication.getApplication()
+ .getApplicationContext()
+ .getSystemService(Context.WINDOW_SERVICE);
+ try {
+ windowManager.removeView(view);
+ } catch (Exception ex) {
+ Log.w(TAG, "removeView: error = " + ex);
+ }
+ }
+
+ public void removeViewImmediate(View view) {
+ WindowManager windowManager =
+ (WindowManager)
+ MatterTvServerApplication.getApplication()
+ .getApplicationContext()
+ .getSystemService(Context.WINDOW_SERVICE);
+ try {
+ windowManager.removeViewImmediate(view);
+ } catch (Exception ex) {
+ Log.w(TAG, "removeViewImmediate: error = " + ex);
+ }
+ }
+
+ private static int convert2Flag(int style) {
+ int flags = WindowManager.LayoutParams.FLAG_DIM_BEHIND;
+ if (style == DIALOG_STYLE_1) {
+ return flags
+ | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
+ | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL;
+ } else if (style == DIALOG_STYLE_2) {
+ return flags;
+ } else if (style == DIALOG_STYLE_3) {
+ return flags
+ | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
+ | WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH;
+ } else {
+ return flags;
+ }
+ }
+
+ public static class Builder {
+
+ private int width = WindowManager.LayoutParams.WRAP_CONTENT;
+ private int height = WindowManager.LayoutParams.WRAP_CONTENT;
+ private float dimAmount = 0.6f;
+
+ private int style = DIALOG_STYLE_1;
+ private boolean cancelable = false;
+
+ public Builder setWidth(int width) {
+ this.width = width;
+ return this;
+ }
+
+ public Builder setHeight(int height) {
+ this.height = height;
+ return this;
+ }
+
+ public Builder setCancelable(boolean mCancelable) {
+ this.cancelable = mCancelable;
+ return this;
+ }
+
+ public Builder setDimAmount(float dimAmount) {
+ this.dimAmount = dimAmount;
+ return this;
+ }
+
+ public Builder setStyle(int style) {
+ this.style = style;
+ return this;
+ }
+
+ public PromptDialogManager build() {
+ return new PromptDialogManager(this);
+ }
+ }
+
+ public interface onBackPressListener {
+ void onBackPress();
+ }
+}
diff --git a/examples/tv-app/android/App/platform-app/src/main/java/com/matter/tv/server/model/PromptCommissionerPasscode.java b/examples/tv-app/android/App/platform-app/src/main/java/com/matter/tv/server/model/PromptCommissionerPasscode.java
new file mode 100644
index 0000000..1a5657f
--- /dev/null
+++ b/examples/tv-app/android/App/platform-app/src/main/java/com/matter/tv/server/model/PromptCommissionerPasscode.java
@@ -0,0 +1,133 @@
+package com.matter.tv.server.model;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+public class PromptCommissionerPasscode implements Parcelable {
+ private int vendorId;
+ private int productId;
+ private String commissioneeName;
+ private long passcode;
+ private int pairingHint;
+ private String pairingInstruction;
+
+ public PromptCommissionerPasscode(
+ int vendorId,
+ int productId,
+ String commissioneeName,
+ long passcode,
+ int pairingHint,
+ String pairingInstruction) {
+ this.vendorId = vendorId;
+ this.productId = productId;
+ this.commissioneeName = commissioneeName;
+ this.passcode = passcode;
+ this.pairingHint = pairingHint;
+ this.pairingInstruction = pairingInstruction;
+ }
+
+ protected PromptCommissionerPasscode(Parcel in) {
+ vendorId = in.readInt();
+ productId = in.readInt();
+ commissioneeName = in.readString();
+ passcode = in.readLong();
+ pairingHint = in.readInt();
+ pairingInstruction = in.readString();
+ }
+
+ public static final Creator<PromptCommissionerPasscode> CREATOR =
+ new Creator<PromptCommissionerPasscode>() {
+ @Override
+ public PromptCommissionerPasscode createFromParcel(Parcel in) {
+ return new PromptCommissionerPasscode(in);
+ }
+
+ @Override
+ public PromptCommissionerPasscode[] newArray(int size) {
+ return new PromptCommissionerPasscode[size];
+ }
+ };
+
+ public int getVendorId() {
+ return vendorId;
+ }
+
+ public void setVendorId(int vendorId) {
+ this.vendorId = vendorId;
+ }
+
+ public int getProductId() {
+ return productId;
+ }
+
+ public void setProductId(int productId) {
+ this.productId = productId;
+ }
+
+ public String getCommissioneeName() {
+ return commissioneeName;
+ }
+
+ public void setCommissioneeName(String commissioneeName) {
+ this.commissioneeName = commissioneeName;
+ }
+
+ public long getPasscode() {
+ return passcode;
+ }
+
+ public void setPasscode(long passcode) {
+ this.passcode = passcode;
+ }
+
+ public int getPairingHint() {
+ return pairingHint;
+ }
+
+ public void setPairingHint(int pairingHint) {
+ this.pairingHint = pairingHint;
+ }
+
+ public String getPairingInstruction() {
+ return pairingInstruction;
+ }
+
+ public void setPairingInstruction(String pairingInstruction) {
+ this.pairingInstruction = pairingInstruction;
+ }
+
+ @Override
+ public String toString() {
+ return "PromptCommissionerPasscode{"
+ + "vendorId="
+ + vendorId
+ + ", productId="
+ + productId
+ + ", commissioneeName='"
+ + commissioneeName
+ + '\''
+ + ", passcode="
+ + passcode
+ + ", pairingHint="
+ + pairingHint
+ + ", pairingInstruction='"
+ + pairingInstruction
+ + '\''
+ + '}';
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel parcel, int i) {
+ parcel.writeInt(vendorId);
+ parcel.writeInt(productId);
+ parcel.writeString(commissioneeName);
+ parcel.writeLong(passcode);
+ parcel.writeInt(pairingHint);
+ parcel.writeString(pairingInstruction);
+ }
+}
diff --git a/examples/tv-app/android/App/platform-app/src/main/java/com/matter/tv/server/service/MatterServant.java b/examples/tv-app/android/App/platform-app/src/main/java/com/matter/tv/server/service/MatterServant.java
index 540798a..c1d479e 100644
--- a/examples/tv-app/android/App/platform-app/src/main/java/com/matter/tv/server/service/MatterServant.java
+++ b/examples/tv-app/android/App/platform-app/src/main/java/com/matter/tv/server/service/MatterServant.java
@@ -17,7 +17,6 @@
*/
package com.matter.tv.server.service;
-import android.app.Activity;
import android.content.Context;
import android.util.Log;
import androidx.annotation.NonNull;
@@ -68,7 +67,6 @@
}
private Context context;
- private Activity activity;
public void init(@NonNull Context context) {
@@ -150,14 +148,6 @@
mIsOn = !mIsOn;
}
- public void setActivity(Activity activity) {
- this.activity = activity;
- }
-
- public Activity getActivity() {
- return activity;
- }
-
public void sendCustomCommand(String customCommand) {
Log.i(MatterServant.class.getName(), customCommand);
// TODO: insert logic ot send custom command here
diff --git a/examples/tv-app/android/App/platform-app/src/main/java/com/matter/tv/server/utils/PxConvert.java b/examples/tv-app/android/App/platform-app/src/main/java/com/matter/tv/server/utils/PxConvert.java
new file mode 100644
index 0000000..5f00cc3
--- /dev/null
+++ b/examples/tv-app/android/App/platform-app/src/main/java/com/matter/tv/server/utils/PxConvert.java
@@ -0,0 +1,13 @@
+package com.matter.tv.server.utils;
+
+import android.content.Context;
+import android.util.TypedValue;
+
+public class PxConvert {
+ public static int dp2px(Context context, float value) {
+ return (int)
+ (TypedValue.applyDimension(
+ TypedValue.COMPLEX_UNIT_DIP, value, context.getResources().getDisplayMetrics())
+ + 0.5f);
+ }
+}
diff --git a/examples/tv-app/android/App/platform-app/src/main/res/drawable/blue_button_background.xml b/examples/tv-app/android/App/platform-app/src/main/res/drawable/blue_button_background.xml
new file mode 100644
index 0000000..0e597b4
--- /dev/null
+++ b/examples/tv-app/android/App/platform-app/src/main/res/drawable/blue_button_background.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_pressed="true">
+ <shape android:shape="rectangle">
+ <solid android:color="@color/blue_btn_bg_pressed_color" />
+ <corners android:radius="6dp"/>
+ </shape>
+ </item>
+ <item>
+ <shape android:shape="rectangle">
+ <solid android:color="@color/blue_btn_bg_color" />
+ <corners android:radius="6dp"/>
+ </shape>
+ </item>
+</selector>
\ No newline at end of file
diff --git a/examples/tv-app/android/App/platform-app/src/main/res/drawable/gray_button_background.xml b/examples/tv-app/android/App/platform-app/src/main/res/drawable/gray_button_background.xml
new file mode 100644
index 0000000..04e807d
--- /dev/null
+++ b/examples/tv-app/android/App/platform-app/src/main/res/drawable/gray_button_background.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_pressed="true">
+ <shape android:shape="rectangle">
+ <solid android:color="@color/gray_btn_bg_pressed_color" />
+ <corners android:radius="6dp"/>
+ </shape>
+ </item>
+ <item>
+ <shape android:shape="rectangle">
+ <solid android:color="@color/gray_btn_bg_color" />
+ <corners android:radius="6dp"/>
+ </shape>
+ </item>
+</selector>
\ No newline at end of file
diff --git a/examples/tv-app/android/App/platform-app/src/main/res/drawable/shape_dialog_background.xml b/examples/tv-app/android/App/platform-app/src/main/res/drawable/shape_dialog_background.xml
new file mode 100644
index 0000000..0f9508b
--- /dev/null
+++ b/examples/tv-app/android/App/platform-app/src/main/res/drawable/shape_dialog_background.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+ <solid android:color="@color/dialog_bg_color" />
+ <corners android:radius="6dp"/>
+</shape>
\ No newline at end of file
diff --git a/examples/tv-app/android/App/platform-app/src/main/res/layout/layout_dialog.xml b/examples/tv-app/android/App/platform-app/src/main/res/layout/layout_dialog.xml
new file mode 100644
index 0000000..6fb96a1
--- /dev/null
+++ b/examples/tv-app/android/App/platform-app/src/main/res/layout/layout_dialog.xml
@@ -0,0 +1,82 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="350dp"
+ android:layout_height="wrap_content"
+ android:background="@drawable/shape_dialog_background">
+
+ <TextView
+ android:id="@+id/tv_title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="10dp"
+ android:textColor="#575757"
+ android:textSize="19sp"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ tools:text="Allow access to Test TV casting app" />
+
+ <TextView
+ android:id="@+id/tv_content"
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_marginHorizontal="10dp"
+ android:layout_marginTop="10dp"
+ android:gravity="center"
+ android:textAlignment="center"
+ android:textColor="#797979"
+ android:textSize="14sp"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/tv_title"
+ tools:text="Test TV casting app is requesting permission to cast to this device, approve?" />
+
+ <View
+ android:id="@+id/v_horizon"
+ android:layout_width="0dp"
+ android:layout_height="1dp"
+ android:layout_marginTop="10dp"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/tv_content" />
+
+ <LinearLayout
+ android:id="@+id/ll_action"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:gravity="center"
+ android:orientation="horizontal"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/v_horizon">
+
+ <Button
+ android:id="@+id/tv_left"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="16dp"
+ style="@style/dialog_blue_button"
+ android:layout_weight="1"
+ android:text="Cancel" />
+
+ <Button
+ android:id="@+id/tv_right"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ style="@style/dialog_blue_button"
+ android:layout_marginStart="48dp"
+ android:layout_marginEnd="16dp"
+ android:layout_weight="1"
+ android:text="Ok" />
+ </LinearLayout>
+
+
+ <Space
+ android:layout_width="wrap_content"
+ android:layout_height="8dp"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/ll_action" />
+</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
diff --git a/examples/tv-app/android/App/platform-app/src/main/res/layout/layout_input_dialog.xml b/examples/tv-app/android/App/platform-app/src/main/res/layout/layout_input_dialog.xml
new file mode 100644
index 0000000..96326c2
--- /dev/null
+++ b/examples/tv-app/android/App/platform-app/src/main/res/layout/layout_input_dialog.xml
@@ -0,0 +1,88 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="400dp"
+ android:layout_height="wrap_content"
+ android:background="@drawable/shape_dialog_background">
+
+ <TextView
+ android:id="@+id/tv_title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="10dp"
+ android:singleLine="true"
+ android:textColor="#575757"
+ android:textSize="19sp"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ tools:text="Allow access to Test TV" />
+
+ <TextView
+ android:id="@+id/tv_content"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginHorizontal="15dp"
+ android:layout_marginTop="10dp"
+ android:textSize="14sp"
+ android:textAlignment="center"
+ android:gravity="center"
+ android:textColor="#797979"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/tv_title"
+ tools:text="Please enter PIN displayed in casting app." />
+
+ <EditText
+ android:id="@+id/et_pin"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginHorizontal="10dp"
+ android:inputType="number"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/tv_content" />
+
+ <View
+ android:id="@+id/v_horizon"
+ android:layout_width="0dp"
+ android:layout_height="1dp"
+ android:layout_marginTop="10dp"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/et_pin" />
+
+ <Button
+ android:id="@+id/tv_left"
+ style="@style/dialog_blue_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="8dp"
+ android:text="Cancel"
+ app:layout_constraintEnd_toStartOf="@+id/tv_right"
+ app:layout_constraintHorizontal_bias="0.5"
+ app:layout_constraintHorizontal_chainStyle="spread"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/v_horizon" />
+
+ <Button
+ android:id="@+id/tv_right"
+ style="@style/dialog_blue_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="10dp"
+ android:layout_marginEnd="8dp"
+ android:text="Ok"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintHorizontal_bias="0.5"
+ app:layout_constraintStart_toEndOf="@+id/tv_left"
+ app:layout_constraintTop_toBottomOf="@id/v_horizon" />
+
+ <Space
+ android:layout_width="wrap_content"
+ android:layout_height="8dp"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/tv_right" />
+</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
diff --git a/examples/tv-app/android/App/platform-app/src/main/res/values/colors.xml b/examples/tv-app/android/App/platform-app/src/main/res/values/colors.xml
index f8c6127..112e295 100644
--- a/examples/tv-app/android/App/platform-app/src/main/res/values/colors.xml
+++ b/examples/tv-app/android/App/platform-app/src/main/res/values/colors.xml
@@ -7,4 +7,11 @@
<color name="teal_700">#FF018786</color>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
+
+ <color name="dialog_bg_color">#FFFFFF</color>
+ <color name="button_text_color">#FFFFFF</color>
+ <color name="gray_btn_bg_color">#D0D0D0</color>
+ <color name="gray_btn_bg_pressed_color">#B6B6B6</color>
+ <color name="blue_btn_bg_color">#AEDEF4</color>
+ <color name="blue_btn_bg_pressed_color">#96BFD2</color>
</resources>
\ No newline at end of file
diff --git a/examples/tv-app/android/App/platform-app/src/main/res/values/themes.xml b/examples/tv-app/android/App/platform-app/src/main/res/values/themes.xml
index 2d74dd9..499da28 100644
--- a/examples/tv-app/android/App/platform-app/src/main/res/values/themes.xml
+++ b/examples/tv-app/android/App/platform-app/src/main/res/values/themes.xml
@@ -13,4 +13,15 @@
<item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
<!-- Customize your theme here. -->
</style>
+
+ <!-- AlertDialog theme. -->
+ <style name="dialog_blue_button" parent="android:Widget.Button">
+ <item name="android:layout_width">wrap_content</item>
+ <item name="android:layout_height">31dp</item>
+ <item name="android:background">@drawable/blue_button_background</item>
+ <item name="android:textSize">14sp</item>
+ <item name="android:paddingLeft">21dp</item>
+ <item name="android:paddingRight">21dp</item>
+ <item name="android:textColor">@color/button_text_color</item>
+ </style>
</resources>
\ No newline at end of file