From 621bbdee21db6d3a561bc7f63483154ed7b5b20d Mon Sep 17 00:00:00 2001 From: Vivien Nicolas Date: Thu, 26 Jan 2023 16:13:50 +0100 Subject: [PATCH 1/9] Update examples/placeholder to supports an interactive websocket mode (#24652) --- examples/placeholder/linux/AppOptions.cpp | 26 ++++ examples/placeholder/linux/AppOptions.h | 2 + examples/placeholder/linux/BUILD.gn | 6 +- .../placeholder/linux/InteractiveServer.cpp | 145 ++++++++++++++++++ .../linux/include/InteractiveServer.h | 47 ++++++ .../linux/include/MatterCallbacks.h | 7 + examples/placeholder/linux/main.cpp | 7 + 7 files changed, 239 insertions(+), 1 deletion(-) create mode 100644 examples/placeholder/linux/InteractiveServer.cpp create mode 100644 examples/placeholder/linux/include/InteractiveServer.h diff --git a/examples/placeholder/linux/AppOptions.cpp b/examples/placeholder/linux/AppOptions.cpp index c780c314194ef9..115f17a4782ed2 100644 --- a/examples/placeholder/linux/AppOptions.cpp +++ b/examples/placeholder/linux/AppOptions.cpp @@ -23,8 +23,12 @@ using chip::ArgParser::OptionSet; using chip::ArgParser::PrintArgError; constexpr uint16_t kOptionDacProviderFilePath = 0xFF01; +constexpr uint16_t kOptionInteractiveMode = 0xFF02; +constexpr uint16_t kOptionInteractiveModePort = 0xFF03; static chip::Credentials::Examples::TestHarnessDACProvider mDacProvider; +static bool gInteractiveMode = false; +static chip::Optional gInteractiveModePort; bool AppOptions::HandleOptions(const char * program, OptionSet * options, int identifier, const char * name, const char * value) { @@ -34,6 +38,12 @@ bool AppOptions::HandleOptions(const char * program, OptionSet * options, int id case kOptionDacProviderFilePath: mDacProvider.Init(value); break; + case kOptionInteractiveMode: + gInteractiveMode = true; + break; + case kOptionInteractiveModePort: + gInteractiveModePort = chip::MakeOptional(static_cast(atoi(value))); + break; default: PrintArgError("%s: INTERNAL ERROR: Unhandled option: %s\n", program, name); retval = false; @@ -47,6 +57,8 @@ OptionSet * AppOptions::GetOptions() { static OptionDef optionsDef[] = { { "dac_provider", chip::ArgParser::kArgumentRequired, kOptionDacProviderFilePath }, + { "interactive", chip::ArgParser::kNoArgument, kOptionInteractiveMode }, + { "port", chip::ArgParser::kArgumentRequired, kOptionInteractiveModePort }, {}, }; @@ -54,6 +66,10 @@ OptionSet * AppOptions::GetOptions() AppOptions::HandleOptions, optionsDef, "PROGRAM OPTIONS", " --dac_provider \n" " A json file with data used by the example dac provider to validate device attestation procedure.\n" + " --interactive\n" + " Enable server interactive mode.\n" + " --port \n" + " Specify the listening port for the server interactive mode.\n" }; return &options; @@ -63,3 +79,13 @@ chip::Credentials::DeviceAttestationCredentialsProvider * AppOptions::GetDACProv { return &mDacProvider; } + +bool AppOptions::GetInteractiveMode() +{ + return gInteractiveMode; +} + +chip::Optional AppOptions::GetInteractiveModePort() +{ + return gInteractiveModePort; +} diff --git a/examples/placeholder/linux/AppOptions.h b/examples/placeholder/linux/AppOptions.h index 3073c66176331f..1451f325184111 100644 --- a/examples/placeholder/linux/AppOptions.h +++ b/examples/placeholder/linux/AppOptions.h @@ -27,6 +27,8 @@ class AppOptions public: static chip::ArgParser::OptionSet * GetOptions(); static chip::Credentials::DeviceAttestationCredentialsProvider * GetDACProvider(); + static bool GetInteractiveMode(); + static chip::Optional GetInteractiveModePort(); private: static bool HandleOptions(const char * program, chip::ArgParser::OptionSet * options, int identifier, const char * name, diff --git a/examples/placeholder/linux/BUILD.gn b/examples/placeholder/linux/BUILD.gn index 4599a450efae87..6b956056c3bc98 100644 --- a/examples/placeholder/linux/BUILD.gn +++ b/examples/placeholder/linux/BUILD.gn @@ -31,6 +31,7 @@ chip_data_model("configuration") { config("includes") { include_dirs = [ ".", + "${chip_root}/examples/common", "include", ] } @@ -38,6 +39,7 @@ config("includes") { executable("chip-${chip_tests_zap_config}") { sources = [ "AppOptions.cpp", + "InteractiveServer.cpp", "main.cpp", "src/bridged-actions-stub.cpp", "static-supported-modes-manager.cpp", @@ -45,6 +47,7 @@ executable("chip-${chip_tests_zap_config}") { deps = [ ":configuration", + "${chip_root}/examples/common/websocket-server", "${chip_root}/examples/platform/linux:app-main", "${chip_root}/src/app/tests/suites/commands/delay", "${chip_root}/src/app/tests/suites/commands/discovery", @@ -54,9 +57,10 @@ executable("chip-${chip_tests_zap_config}") { "${chip_root}/src/lib", "${chip_root}/src/lib/support:testing", # For sleepMillis. TODO: this is # odd and should be fixed + "${chip_root}/third_party/jsoncpp", ] - include_dirs = [ "include" ] + public_configs = [ ":includes" ] cflags = [ "-Wconversion" ] diff --git a/examples/placeholder/linux/InteractiveServer.cpp b/examples/placeholder/linux/InteractiveServer.cpp new file mode 100644 index 00000000000000..e780be8ef4dda3 --- /dev/null +++ b/examples/placeholder/linux/InteractiveServer.cpp @@ -0,0 +1,145 @@ +/* + * + * Copyright (c) 2023 Project CHIP Authors + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "InteractiveServer.h" + +#include +#include + +using namespace chip::DeviceLayer; + +namespace { +constexpr const char * kClusterIdKey = "clusterId"; +constexpr const char * kEndpointIdKey = "endpointId"; +constexpr const char * kAttributeIdKey = "attributeId"; +constexpr const char * kWaitTypeKey = "waitType"; +constexpr const char * kAttributeWriteKey = "writeAttribute"; +constexpr const char * kAttributeReadKey = "readAttribute"; +constexpr const char * kCommandIdKey = "commandId"; +constexpr const char * kWaitForCommissioningCommand = "WaitForCommissioning"; + +std::string JsonToString(Json::Value & json) +{ + Json::FastWriter writer; + writer.omitEndingLineFeed(); + return writer.write(json); +} + +void OnPlatformEvent(const ChipDeviceEvent * event, intptr_t arg); + +void OnCommissioningComplete(intptr_t context) +{ + PlatformMgr().RemoveEventHandler(OnPlatformEvent); + InteractiveServer::GetInstance().CommissioningComplete(); +} + +void OnPlatformEvent(const ChipDeviceEvent * event, intptr_t arg) +{ + switch (event->Type) + { + case DeviceEventType::kCommissioningComplete: + PlatformMgr().ScheduleWork(OnCommissioningComplete, arg); + break; + } +} +} // namespace + +InteractiveServer * InteractiveServer::instance = nullptr; +InteractiveServer & InteractiveServer::GetInstance() +{ + if (instance == nullptr) + { + instance = new InteractiveServer(); + } + return *instance; +} + +void InteractiveServer::Run(const chip::Optional port) +{ + mIsReady = false; + wsThread = std::thread(&WebSocketServer::Run, &mWebSocketServer, port, this); +} + +bool InteractiveServer::OnWebSocketMessageReceived(char * msg) +{ + ChipLogError(chipTool, "Receive message: %s", msg); + if (strcmp(msg, kWaitForCommissioningCommand) == 0) + { + mIsReady = false; + PlatformMgr().AddEventHandler(OnPlatformEvent); + } + else + { + mIsReady = true; + } + return true; +} + +bool InteractiveServer::Command(const chip::app::ConcreteCommandPath & path) +{ + VerifyOrReturnValue(mIsReady, false); + + Json::Value value; + value[kClusterIdKey] = path.mClusterId; + value[kEndpointIdKey] = path.mEndpointId; + value[kCommandIdKey] = path.mCommandId; + + auto valueStr = JsonToString(value); + LogErrorOnFailure(mWebSocketServer.Send(valueStr.c_str())); + return mIsReady; +} + +bool InteractiveServer::ReadAttribute(const chip::app::ConcreteAttributePath & path) +{ + VerifyOrReturnValue(mIsReady, false); + + Json::Value value; + value[kClusterIdKey] = path.mClusterId; + value[kEndpointIdKey] = path.mEndpointId; + value[kAttributeIdKey] = path.mAttributeId; + value[kWaitTypeKey] = kAttributeReadKey; + + auto valueStr = JsonToString(value); + LogErrorOnFailure(mWebSocketServer.Send(valueStr.c_str())); + return mIsReady; +} + +bool InteractiveServer::WriteAttribute(const chip::app::ConcreteAttributePath & path) +{ + VerifyOrReturnValue(mIsReady, false); + + Json::Value value; + value[kClusterIdKey] = path.mClusterId; + value[kEndpointIdKey] = path.mEndpointId; + value[kAttributeIdKey] = path.mAttributeId; + value[kWaitTypeKey] = kAttributeWriteKey; + + auto valueStr = JsonToString(value); + LogErrorOnFailure(mWebSocketServer.Send(valueStr.c_str())); + return mIsReady; +} + +void InteractiveServer::CommissioningComplete() +{ + VerifyOrReturn(!mIsReady); + mIsReady = true; + + Json::Value value = Json::objectValue; + auto valueStr = JsonToString(value); + LogErrorOnFailure(mWebSocketServer.Send(valueStr.c_str())); +} diff --git a/examples/placeholder/linux/include/InteractiveServer.h b/examples/placeholder/linux/include/InteractiveServer.h new file mode 100644 index 00000000000000..9b454fc329362f --- /dev/null +++ b/examples/placeholder/linux/include/InteractiveServer.h @@ -0,0 +1,47 @@ +/* + * + * Copyright (c) 2023 Project CHIP Authors + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include +#include + +class InteractiveServer : public WebSocketServerDelegate +{ +public: + static InteractiveServer & GetInstance(); + void Run(const chip::Optional port); + + bool Command(const chip::app::ConcreteCommandPath & path); + bool ReadAttribute(const chip::app::ConcreteAttributePath & path); + bool WriteAttribute(const chip::app::ConcreteAttributePath & path); + void CommissioningComplete(); + + /////////// WebSocketServerDelegate Interface ///////// + bool OnWebSocketMessageReceived(char * msg) override; + +private: + InteractiveServer(){}; + static InteractiveServer * instance; + + WebSocketServer mWebSocketServer; + std::thread wsThread; + bool mIsReady; +}; diff --git a/examples/placeholder/linux/include/MatterCallbacks.h b/examples/placeholder/linux/include/MatterCallbacks.h index a21a3b8bdc9ceb..0ddddb8f7d2ae9 100644 --- a/examples/placeholder/linux/include/MatterCallbacks.h +++ b/examples/placeholder/linux/include/MatterCallbacks.h @@ -18,6 +18,7 @@ #pragma once +#include "InteractiveServer.h" #include "Options.h" #include @@ -55,6 +56,8 @@ TestCommand * GetTargetTest() void MatterPostCommandReceivedCallback(const chip::app::ConcreteCommandPath & commandPath, const chip::Access::SubjectDescriptor & subjectDescriptor) { + VerifyOrReturn(!InteractiveServer::GetInstance().Command(commandPath)); + auto test = GetTargetTest(); VerifyOrReturn(test != nullptr && test->isRunning); @@ -66,6 +69,8 @@ void MatterPostCommandReceivedCallback(const chip::app::ConcreteCommandPath & co void MatterPostAttributeReadCallback(const chip::app::ConcreteAttributePath & attributePath) { + VerifyOrReturn(!InteractiveServer::GetInstance().ReadAttribute(attributePath)); + auto test = GetTargetTest(); VerifyOrReturn(test != nullptr && test->isRunning); @@ -77,6 +82,8 @@ void MatterPostAttributeReadCallback(const chip::app::ConcreteAttributePath & at void MatterPostAttributeWriteCallback(const chip::app::ConcreteAttributePath & attributePath) { + VerifyOrReturn(!InteractiveServer::GetInstance().WriteAttribute(attributePath)); + auto test = GetTargetTest(); VerifyOrReturn(test != nullptr && test->isRunning); diff --git a/examples/placeholder/linux/main.cpp b/examples/placeholder/linux/main.cpp index 627d24464e2bab..eb43b7ece228f7 100644 --- a/examples/placeholder/linux/main.cpp +++ b/examples/placeholder/linux/main.cpp @@ -33,6 +33,13 @@ int main(int argc, char * argv[]) } LinuxDeviceOptions::GetInstance().dacProvider = AppOptions::GetDACProvider(); + + auto & server = InteractiveServer::GetInstance(); + if (AppOptions::GetInteractiveMode()) + { + server.Run(AppOptions::GetInteractiveModePort()); + } + ChipLinuxAppMainLoop(); return 0; } From dfa7c9d0a921ad5d07a8813efd175d38465a07ab Mon Sep 17 00:00:00 2001 From: Andrei Litvin Date: Thu, 26 Jan 2023 10:14:23 -0500 Subject: [PATCH 2/9] Add a script to simplify getting relevant zap binary if not manually installed (#24651) * Start writing a zap download script. Not yet functional * Restyle * Started detecting zap version to use * Added some functionality - at least zap release download seems to work * Restyle * Minimal documentation for the release download * Add support for zap development version checkout as well * Restyle * Remove some left over comments * Switch download default to project_root/.zap --- .gitignore | 3 + scripts/tools/zap/zap_download.py | 224 ++++++++++++++++++++++++++++++ 2 files changed, 227 insertions(+) create mode 100755 scripts/tools/zap/zap_download.py diff --git a/.gitignore b/.gitignore index 8d4ea014d762c6..5c9f408a1cb968 100644 --- a/.gitignore +++ b/.gitignore @@ -71,3 +71,6 @@ compile_commands.json # log files *.log examples/thermostat/ameba/build + +# Downloaded zap without a pigweed root (via zap_download.py) +.zap diff --git a/scripts/tools/zap/zap_download.py b/scripts/tools/zap/zap_download.py new file mode 100755 index 00000000000000..951691d65fbd19 --- /dev/null +++ b/scripts/tools/zap/zap_download.py @@ -0,0 +1,224 @@ +#!/usr/bin/env python3 + +# Copyright (c) 2023 Project CHIP Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import enum +import io +import logging +import os +import re +import shlex +import shutil +import subprocess +import sys +import zipfile +from typing import Optional + +import click +import requests + +try: + import coloredlogs + _has_coloredlogs = True +except: + _has_coloredlogs = False + +# Supported log levels, mapping string values required for argument +# parsing into logging constants +__LOG_LEVELS__ = { + 'debug': logging.DEBUG, + 'info': logging.INFO, + 'warn': logging.WARN, + 'fatal': logging.FATAL, +} + + +class DownloadType(enum.Enum): + RELEASE = enum.auto() # asking for a zip file release download + SOURCE = enum.auto() # asking for a source download (will work on arm64 for example) + + +def _GetDefaultExtractRoot(): + if 'PW_ENVIRONMENT_ROOT' in os.environ: + return os.environ['PW_ENVIRONMENT_ROOT'] + else: + return ".zap" + + +def _LogPipeLines(pipe, prefix): + l = logging.getLogger().getChild(prefix) + for line in iter(pipe.readline, b''): + line = line.strip().decode('utf-8', errors="ignore") + l.info('%s' % line) + + +def _ExecuteProcess(cmd, cwd): + logging.info('Executing %r in %s' % (cmd, cwd)) + + process = subprocess.Popen( + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + cwd=cwd) + + with process.stdout: + _LogPipeLines(process.stdout, cmd[0]) + + exitcode = process.wait() + if exitcode != 0: + raise Exception("Error executing process: %d" % exitcode) + + +def _SetupSourceZap(install_directory: str, zap_version: str): + if os.path.exists(install_directory): + logging.warning("Completely re-creating %s", install_directory) + shutil.rmtree(install_directory) + + os.makedirs(install_directory, exist_ok=True) + + _ExecuteProcess( + f"git clone --depth 1 --branch {zap_version} https://github.com/project-chip/zap.git .".split(), + install_directory + ) + + _ExecuteProcess(f"npm ci".split(), install_directory) + + +def _SetupReleaseZap(install_directory: str, zap_version: str): + """ + Downloads the given [zap_version] into "[install_directory]/zap-[zap_version]/". + + Will download the given release from github releases. + """ + + if sys.platform == 'linux': + zap_platform = 'linux' + elif sys.platform == 'darwin': + zap_platform = 'mac' + else: + raise Exception('Unknown platform - do not know what zip file to download.') + + url = f"https://github.com/project-chip/zap/releases/download/{zap_version}/zap-{zap_platform}.zip" + + logging.info("Fetching: %s", url) + + r = requests.get(url, stream=True) + z = zipfile.ZipFile(io.BytesIO(r.content)) + + logging.info("Data downloaded, extracting ...") + z.extractall(install_directory) + logging.info("Done extracting.") + + +def _GetZapVersionToUse(project_root): + """ + Heuristic to figure out what zap version should be used. + + Looks at the given project root and tries to figure out the zap tag/version. + """ + + # We have several locations for zap versioning: + # - CI is likely the most reliable as long as we use the "latest build" + # - zap_execution.py is what is currently used, but it is a minimum version + # + # Based on the above, we assume CI is using the latest build (will not be + # out of sync more than a few days) and even if it is not, zap is often + # backwards compatible (new features added, but codegen should not change + # that often for fixed inputs) + # + # This heuristic may be bad at times, however then you can also override the + # version in command line parameters + + match_re = re.compile(r'.*ENV\s+ZAP_VERSION=([^# ]*)') + + docker_path = os.path.join(project_root, "integrations/docker/images/chip-build/Dockerfile") + + with open(docker_path, 'rt') as f: + for l in f.readlines(): + l = l.strip() + m = match_re.match(l) + if not m: + continue + return m.group(1) + + raise Exception(f"Failed to determine version from {docker_path}") + + +@click.command() +@click.option( + '--log-level', + default='INFO', + show_default=True, + type=click.Choice(__LOG_LEVELS__.keys(), case_sensitive=False), + callback=lambda c, p, v: __LOG_LEVELS__[v], + help='Determines the verbosity of script output') +@click.option( + '--sdk-root', + default=os.path.realpath(os.path.join(os.path.dirname(os.path.realpath(__file__)), '..', '..', '..')), + show_default=True, + help='Path to the SDK root (where zap versioning exists).') +@click.option( + '--extract-root', + default=_GetDefaultExtractRoot(), + show_default=True, + help='Directory where too unpack/checkout zap') +@click.option( + '--zap-version', + default=None, + help='Force to checkout this zap version instead of trying to auto-detect') +@click.option( + '--zap', + default='RELEASE', + show_default=True, + type=click.Choice(DownloadType.__members__, case_sensitive=False), + callback=lambda c, p, v: getattr(DownloadType, v), + help='What type of zap download to perform') +def main(log_level: str, sdk_root: str, extract_root: str, zap_version: Optional[str], zap: DownloadType): + if _has_coloredlogs: + coloredlogs.install(level=log_level, fmt='%(asctime)s %(name)s %(levelname)-7s %(message)s') + else: + logging.basicConfig( + level=log_level, + format='%(asctime)s %(name)s %(levelname)-7s %(message)s', + datefmt='%Y-%m-%d %H:%M:%S' + ) + + if extract_root == ".zap": + # Place .zap in the project root + extract_root = os.path.join(sdk_root, extract_root) + + if not zap_version: + zap_version = _GetZapVersionToUse(sdk_root) + logging.info('Found required zap version to be: %s' % zap_version) + + logging.debug('User requested to download a %s zap version %s into %s', zap, zap_version, extract_root) + + install_directory = os.path.join(extract_root, f"zap-{zap_version}") + + if zap == DownloadType.SOURCE: + install_directory = install_directory + "-src" + _SetupSourceZap(install_directory, zap_version) + + # Make sure the results can be used in scripts + print(f"export ZAP_DEVELOPMENT_PATH={shlex.quote(install_directory)}") + else: + _SetupReleaseZap(install_directory, zap_version) + + # Make sure the results can be used in scripts + print(f"export ZAP_INSTALL_PATH={shlex.quote(install_directory)}") + + +if __name__ == '__main__': + main() From 0c2b095200ca9b263e0bcd8879a4aaf8d1343ded Mon Sep 17 00:00:00 2001 From: amitnj <74272437+amitnj@users.noreply.github.com> Date: Thu, 26 Jan 2023 09:03:55 -0800 Subject: [PATCH 3/9] Add support for account login cluster for content apps. (#24628) * Add support for account login cluster for content apps. * Use JniUtfString. Co-authored-by: chrisdecenzo <61757564+chrisdecenzo@users.noreply.github.com> --- .../java/com/matter/tv/app/api/Clusters.java | 31 +++ .../contentapp/CommandResponseHolder.java | 60 +++++ .../com/example/contentapp/MainActivity.java | 27 +- .../receiver/MatterCommandReceiver.java | 4 +- .../src/main/res/layout/activity_main.xml | 115 +++++---- .../account-login/AccountLoginManager.cpp | 43 +++- .../account-login/AccountLoginManager.h | 21 +- examples/tv-app/android/java/AppImpl.cpp | 18 +- examples/tv-app/android/java/AppImpl.h | 29 ++- .../java/ContentAppCommandDelegate.cpp | 233 ++++++++++++++---- .../android/java/ContentAppCommandDelegate.h | 17 +- 11 files changed, 473 insertions(+), 125 deletions(-) create mode 100644 examples/tv-app/android/App/content-app/src/main/java/com/example/contentapp/CommandResponseHolder.java diff --git a/examples/tv-app/android/App/common-api/src/main/java/com/matter/tv/app/api/Clusters.java b/examples/tv-app/android/App/common-api/src/main/java/com/matter/tv/app/api/Clusters.java index 93cd816eb95984..d3fffe4e684873 100644 --- a/examples/tv-app/android/App/common-api/src/main/java/com/matter/tv/app/api/Clusters.java +++ b/examples/tv-app/android/App/common-api/src/main/java/com/matter/tv/app/api/Clusters.java @@ -5,7 +5,38 @@ * the rest of the media clusters TODO : Maybe generate using ZAP tool */ public class Clusters { + // Clusters + public static class AccountLogin { + public static final int Id = 0x050E; + + public static class Commands { + public static class GetSetupPIN { + public static final int ID = 0x00; + + public static class Fields { + public static final int TempAccountIdentifier = 0x00; + } + } + + public static class GetSetupPINResponse { + public static final int ID = 0x01; + + public static class Fields { + public static final int SetupPIN = 0x00; + } + } + + public static class Login { + public static final int ID = 0x02; + } + + public static class Logout { + public static final int ID = 0x03; + } + } + } + public static class MediaPlayback { public static final int Id = 0x0506; diff --git a/examples/tv-app/android/App/content-app/src/main/java/com/example/contentapp/CommandResponseHolder.java b/examples/tv-app/android/App/content-app/src/main/java/com/example/contentapp/CommandResponseHolder.java new file mode 100644 index 00000000000000..aaaeaa0cba7005 --- /dev/null +++ b/examples/tv-app/android/App/content-app/src/main/java/com/example/contentapp/CommandResponseHolder.java @@ -0,0 +1,60 @@ +package com.example.contentapp; + +import android.util.Log; +import com.matter.tv.app.api.Clusters; +import java.util.HashMap; +import java.util.Map; + +/** Class to hold attribute values to help test attribute read and subscribe use cases. */ +public class CommandResponseHolder { + private Map> responseValues = new HashMap<>(); + private static final String TAG = "CommandResponseHolder"; + private static final Long DEFAULT_COMMAND = -1L; + + private static CommandResponseHolder instance = new CommandResponseHolder(); + + private CommandResponseHolder() { + // Setting up responses + setResponseValue( + Clusters.ContentLauncher.Id, + DEFAULT_COMMAND, + "{\"0\":0, \"1\":\"custom response from content app for content launcher\"}"); + setResponseValue( + Clusters.TargetNavigator.Id, + DEFAULT_COMMAND, + "{\"0\":0, \"1\":\"custom response from content app for target navigator\"}"); + setResponseValue( + Clusters.MediaPlayback.Id, + DEFAULT_COMMAND, + "{\"0\":0, \"1\":\"custom response from content app for media playback\"}"); + setResponseValue( + Clusters.AccountLogin.Id, + Clusters.AccountLogin.Commands.GetSetupPIN.ID, + "{\"0\":\"12345678\"}"); + }; + + public static CommandResponseHolder getInstance() { + return instance; + } + + public void setResponseValue(long clusterId, long commandId, String value) { + if (value == null) { + Log.d(TAG, "Setting null for cluster " + clusterId + " command " + commandId); + } + Map responses = responseValues.get(clusterId); + if (responses == null) { + responses = new HashMap<>(); + responseValues.put(clusterId, responses); + } + responses.put(commandId, value); + } + + public String getCommandResponse(long clusterId, long commandId) { + Map responses = responseValues.get(clusterId); + String response = responses.get(commandId); + if (response == null) { + response = responses.get(DEFAULT_COMMAND); + } + return response; + } +} diff --git a/examples/tv-app/android/App/content-app/src/main/java/com/example/contentapp/MainActivity.java b/examples/tv-app/android/App/content-app/src/main/java/com/example/contentapp/MainActivity.java index 70011e726bda38..09731c67139a5f 100644 --- a/examples/tv-app/android/App/content-app/src/main/java/com/example/contentapp/MainActivity.java +++ b/examples/tv-app/android/App/content-app/src/main/java/com/example/contentapp/MainActivity.java @@ -4,6 +4,7 @@ import android.os.Bundle; import android.widget.ArrayAdapter; import android.widget.Button; +import android.widget.EditText; import android.widget.Spinner; import android.widget.TextView; import androidx.appcompat.app.AppCompatActivity; @@ -27,6 +28,7 @@ public class MainActivity extends AppCompatActivity { private static final String ATTR_TL_LONG = "Target List : LONG"; private static final String ATTR_TL_SHORT = "Target List : SHORT"; private final ExecutorService executorService = Executors.newSingleThreadExecutor(); + private String setupPIN = ""; @Override protected void onCreate(Bundle savedInstanceState) { @@ -43,9 +45,30 @@ protected void onCreate(Bundle savedInstanceState) { TextView textView = (TextView) findViewById(R.id.commandTextView); textView.setText("Command Payload : " + command); - Button sendMessageButton = findViewById(R.id.sendMessageButton); + Button setupPINButton = findViewById(R.id.setupPINButton); + if (!setupPIN.isEmpty()) { + EditText pinText = findViewById(R.id.setupPINText); + pinText.setText(setupPIN); + } + setupPINButton.setOnClickListener( + view -> { + EditText pinText = findViewById(R.id.setupPINText); + String pinStr = pinText.getText().toString(); + setupPIN = pinStr; + CommandResponseHolder.getInstance() + .setResponseValue( + Clusters.AccountLogin.Id, + Clusters.AccountLogin.Commands.GetSetupPIN.ID, + "{\"" + + Clusters.AccountLogin.Commands.GetSetupPINResponse.Fields.SetupPIN + + "\":\"" + + pinStr + + "\"}"); + }); + + Button attributeUpdateButton = findViewById(R.id.updateAttributeButton); - sendMessageButton.setOnClickListener( + attributeUpdateButton.setOnClickListener( view -> { Spinner dropdown = findViewById(R.id.spinnerAttribute); String attribute = (String) dropdown.getSelectedItem(); diff --git a/examples/tv-app/android/App/content-app/src/main/java/com/example/contentapp/receiver/MatterCommandReceiver.java b/examples/tv-app/android/App/content-app/src/main/java/com/example/contentapp/receiver/MatterCommandReceiver.java index 2fda386654a5c1..1ff92853ec8648 100644 --- a/examples/tv-app/android/App/content-app/src/main/java/com/example/contentapp/receiver/MatterCommandReceiver.java +++ b/examples/tv-app/android/App/content-app/src/main/java/com/example/contentapp/receiver/MatterCommandReceiver.java @@ -6,6 +6,7 @@ import android.content.Intent; import android.util.Log; import com.example.contentapp.AttributeHolder; +import com.example.contentapp.CommandResponseHolder; import com.example.contentapp.MainActivity; import com.example.contentapp.matter.MatterAgentClient; import com.matter.tv.app.api.MatterIntentConstants; @@ -43,7 +44,8 @@ public void onReceive(Context context, Intent intent) { .append(command) .toString(); Log.d(TAG, message); - String response = "{\"0\":1, \"1\":\"custom response from content app\"}"; + String response = + CommandResponseHolder.getInstance().getCommandResponse(clusterId, commandId); Intent in = new Intent(context, MainActivity.class); in.putExtra(MatterIntentConstants.EXTRA_COMMAND_PAYLOAD, command); diff --git a/examples/tv-app/android/App/content-app/src/main/res/layout/activity_main.xml b/examples/tv-app/android/App/content-app/src/main/res/layout/activity_main.xml index dadcd97935481a..fe8b3003c94ca0 100644 --- a/examples/tv-app/android/App/content-app/src/main/res/layout/activity_main.xml +++ b/examples/tv-app/android/App/content-app/src/main/res/layout/activity_main.xml @@ -1,55 +1,82 @@ - - + android:gravity="left" + android:orientation="vertical" + android:padding="10sp"> - - - - - -