docs: archive ABSDK Android sample

This commit is contained in:
KremeCN
2026-08-22 15:48:16 +08:00
parent 3c7c6002df
commit 6f6f3755b5
106 changed files with 3012 additions and 0 deletions
@@ -0,0 +1 @@
/build
@@ -0,0 +1,59 @@
plugins {
alias(libs.plugins.android.application)
}
android {
namespace 'tr.dhee.abox.demo'
compileSdk 34
// SDK(libABSDK0630.jar) 内部依赖 Apache HttpClient(org.apache.http)。
// 自包含方案: 四个 Apache jar 直接打进 APK, 不依赖设备系统库(任意机型可装)。
defaultConfig {
applicationId "tr.dhee.abox.demo"
minSdk 21
targetSdk 34
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_11
targetCompatibility JavaVersion.VERSION_11
}
buildFeatures {
viewBinding true
}
// 多个 Apache HTTP 相关 jar 均带 META-INF 元数据文件, 打包进同一 APK 会冲突, 排除这些重复元数据(不影响运行时类)。
packagingOptions {
resources {
excludes += ['META-INF/DEPENDENCIES', 'META-INF/LICENSE', 'META-INF/LICENSE.txt',
'META-INF/NOTICE', 'META-INF/NOTICE.txt', 'META-INF/ASL2.0',
'META-INF/license.txt', 'META-INF/notice.txt']
}
}
}
dependencies {
// 引入 libs 下全部 jar: SDK(libABSDK0630.jar) + Apache HttpClient 四件套(自包含, 不依赖系统库)。
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation libs.activity.ktx
implementation libs.annotation
implementation libs.appcompat
implementation libs.constraintlayout
implementation libs.lifecycle.livedata.ktx
implementation libs.lifecycle.viewmodel.ktx
implementation libs.material
testImplementation libs.junit
androidTestImplementation libs.espresso.core
androidTestImplementation libs.ext.junit
}
@@ -0,0 +1,26 @@
package tr.dhee.abox.demo;
import android.content.Context;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
import static org.junit.Assert.*;
/**
* Instrumented test, which will execute on an Android device.
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
@RunWith(AndroidJUnit4.class)
public class ExampleInstrumentedTest {
@Test
public void useAppContext() {
// Context of the app under test.
Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
assertEquals("tr.dhee.abox.demo", appContext.getPackageName());
}
}
@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<application
android:allowBackup="true"
android:usesCleartextTraffic="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Aboxtest">
<activity
android:name=".ui.login.LoginActivity"
android:exported="true"
android:label="@string/app_name"
android:windowSoftInputMode="adjustResize">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".ui.switchcontrol.SwitchControlActivity"
android:exported="false"
android:label="@string/switch_control_title" />
</application>
</manifest>
@@ -0,0 +1,43 @@
package tr.dhee.abox.demo.data;
import tr.dhee.abox.demo.data.model.LoggedInUser;
import tr.dhee.abox.demo.utils.AboxConstants;
import com.dhc.absdk.ABRet;
import com.dhc.absdk.ABSDK;
import java.io.IOException;
/**
* Class that handles authentication w/ login credentials and retrieves user information.
* 接 base 真登录: 调用 libABSDK0630.jar 的 ABSDK.getInstance().loginWithUsername()。
*/
public class LoginDataSource {
public Result<LoggedInUser> login(String username, String password) {
try {
// base 登录核心: 同步阻塞网络调用, 在 LoginRepository 的后台线程执行(不会 ANR)
ABRet abRet = ABSDK.getInstance().loginWithUsername(username, password);
if (abRet != null && "00000".equals(abRet.getCode())) {
// 成功: 用用户名作为展示名(与 base 行为一致, base 未返回用户信息)
LoggedInUser user = new LoggedInUser(username, username);
return new Result.Success<>(user);
} else {
// 失败: 用 AboxConstants.codeMap 把返回码映射成中文提示
String code = abRet != null ? abRet.getCode() : "unknown";
String msg = AboxConstants.codeMap.get(code);
if (msg == null) {
msg = "登录失败: " + code;
}
return new Result.Error(new IOException(msg));
}
} catch (Exception e) {
// 网络异常 / SDK 初始化异常等
return new Result.Error(new IOException("登录异常: " + e.getMessage(), e));
}
}
public void logout() {
// TODO: revoke authentication
}
}
@@ -0,0 +1,66 @@
package tr.dhee.abox.demo.data;
import tr.dhee.abox.demo.data.model.LoggedInUser;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
/**
* Class that requests authentication and user information from the remote data source and
* maintains an in-memory cache of login status and user credentials information.
* 登录在后台线程执行(SDK 的 loginWithUsername 是同步网络调用, 不能在主线程跑, 否则 ANR)。
*/
public class LoginRepository {
private static volatile LoginRepository instance;
private LoginDataSource dataSource;
private final Executor executor = Executors.newSingleThreadExecutor();
// If user credentials will be cached in local storage, it is recommended it be encrypted
// @see https://developer.android.com/training/articles/keystore
private LoggedInUser user = null;
// private constructor : singleton access
private LoginRepository(LoginDataSource dataSource) {
this.dataSource = dataSource;
}
public static LoginRepository getInstance(LoginDataSource dataSource) {
if (instance == null) {
instance = new LoginRepository(dataSource);
}
return instance;
}
public boolean isLoggedIn() {
return user != null;
}
public void logout() {
user = null;
dataSource.logout();
}
private void setLoggedInUser(LoggedInUser user) {
this.user = user;
// If user credentials will be cached in local storage, it is recommended it be encrypted
// @see https://developer.android.com/training/articles/keystore
}
public void login(String username, String password, LoginCallback callback) {
executor.execute(() -> {
Result<LoggedInUser> result = dataSource.login(username, password);
if (result instanceof Result.Success) {
setLoggedInUser(((Result.Success<LoggedInUser>) result).getData());
}
if (callback != null) {
callback.onComplete(result);
}
});
}
public interface LoginCallback {
void onComplete(Result<LoggedInUser> result);
}
}
@@ -0,0 +1,48 @@
package tr.dhee.abox.demo.data;
/**
* A generic class that holds a result success w/ data or an error exception.
*/
public class Result<T> {
// hide the private constructor to limit subclass types (Success, Error)
private Result() {
}
@Override
public String toString() {
if (this instanceof Result.Success) {
Result.Success success = (Result.Success) this;
return "Success[data=" + success.getData().toString() + "]";
} else if (this instanceof Result.Error) {
Result.Error error = (Result.Error) this;
return "Error[exception=" + error.getError().toString() + "]";
}
return "";
}
// Success sub-class
public final static class Success<T> extends Result {
private T data;
public Success(T data) {
this.data = data;
}
public T getData() {
return this.data;
}
}
// Error sub-class
public final static class Error extends Result {
private Exception error;
public Error(Exception error) {
this.error = error;
}
public Exception getError() {
return this.error;
}
}
}
@@ -0,0 +1,45 @@
package tr.dhee.abox.demo.data;
import com.dhc.absdk.ABRet;
import com.dhc.absdk.ABSDK;
import tr.dhee.abox.demo.utils.AboxConstants;
import java.io.IOException;
/**
* Class that handles socket device control via sockCtrl API.
* Must be called after login (ABSDK already has the session).
*/
public class SwitchDataSource {
public Result<String> sockCtrl(String soDevName, String status) {
try {
// 同步阻塞调用 SDK 的 sockCtrl 接口, 在 SwitchRepository 的后台线程执行
ABRet abRet = ABSDK.getInstance().sockCtrl(soDevName, status);
if (abRet != null && "00000".equals(abRet.getCode())) {
// 成功: 返回状态描述
String desc = "操作成功: " + status;
return new Result.Success<>(desc);
} else {
// 失败: 映射返回码到中文提示, 并始终携带原始错误码
String code = abRet != null ? abRet.getCode() : "unknown";
String abMsg = abRet != null ? abRet.getMsg() : "";
String mapped = AboxConstants.codeMap.get(code);
String msg;
if (mapped != null) {
msg = mapped + " (" + code + ")";
} else if (abMsg != null && !abMsg.isEmpty()) {
msg = abMsg + " (" + code + ")";
} else {
msg = "控制失败: " + code;
}
return new Result.Error(new IOException(msg));
}
} catch (Exception e) {
// 网络异常 / SDK 异常等
String detail = e.getMessage() != null ? e.getMessage() : e.getClass().getSimpleName();
return new Result.Error(new IOException("控制异常: " + detail, e));
}
}
}
@@ -0,0 +1,55 @@
package tr.dhee.abox.demo.data;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
/**
* Class that handles socket device control via SwitchDataSource.
* Executes SDK calls on a background thread to avoid ANR.
*/
public class SwitchRepository {
private static volatile SwitchRepository instance;
private final SwitchDataSource dataSource;
private final Executor executor = Executors.newSingleThreadExecutor();
private SwitchRepository(SwitchDataSource dataSource) {
this.dataSource = dataSource;
}
public static SwitchRepository getInstance(SwitchDataSource dataSource) {
if (instance == null) {
instance = new SwitchRepository(dataSource);
}
return instance;
}
/**
* 控制插座开关, 在后台线程执行 SDK 调用
*
* @param soDevName 插座设备名称
* @param status 状态: "1"(开) 或 "0"(关)
* @param callback 结果回调 (在主线程调用)
*/
public void sockCtrl(String soDevName, String status, SwitchCallback callback) {
executor.execute(() -> {
Result<String> result = dataSource.sockCtrl(soDevName, status);
if (callback != null) {
callback.onComplete(result);
}
});
executor.execute(new Runnable() {
@Override
public void run() {
}
});
}
public interface SwitchCallback {
void onComplete(Result<String> result);
}
}
@@ -0,0 +1,23 @@
package tr.dhee.abox.demo.data.model;
/**
* Data class that captures user information for logged in users retrieved from LoginRepository
*/
public class LoggedInUser {
private String userId;
private String displayName;
public LoggedInUser(String userId, String displayName) {
this.userId = userId;
this.displayName = displayName;
}
public String getUserId() {
return userId;
}
public String getDisplayName() {
return displayName;
}
}
@@ -0,0 +1,17 @@
package tr.dhee.abox.demo.ui.login;
/**
* Class exposing authenticated user details to the UI.
*/
class LoggedInUserView {
private String displayName;
//... other data fields that may be accessible to the UI
LoggedInUserView(String displayName) {
this.displayName = displayName;
}
String getDisplayName() {
return displayName;
}
}
@@ -0,0 +1,173 @@
package tr.dhee.abox.demo.ui.login;
import androidx.activity.EdgeToEdge;
import androidx.lifecycle.Observer;
import androidx.lifecycle.ViewModelProvider;
import android.content.Intent;
import android.os.Bundle;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;
import android.content.Context;
import android.content.SharedPreferences;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.KeyEvent;
import android.view.View;
import android.view.inputmethod.EditorInfo;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.EditText;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;
import tr.dhee.abox.demo.R;
import tr.dhee.abox.demo.ui.switchcontrol.SwitchControlActivity;
import tr.dhee.abox.demo.databinding.ActivityLoginBinding;
import tr.dhee.abox.demo.utils.AboxConstants;
public class LoginActivity extends AppCompatActivity {
private LoginViewModel loginViewModel;
private ActivityLoginBinding binding;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
EdgeToEdge.enable(this);
binding = ActivityLoginBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
ViewCompat.setOnApplyWindowInsetsListener(binding.main, (v, insets) -> {
Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
return insets;
});
loginViewModel = new ViewModelProvider(this, new LoginViewModelFactory())
.get(LoginViewModel.class);
final EditText usernameEditText = binding.username;
final EditText passwordEditText = binding.password;
final CheckBox checkBoxAutoLogin = binding.checkBoxAutoLogin;
final Button loginButton = binding.login;
final ProgressBar loadingProgressBar = binding.loading;
// 自动登录: 读取上次保存的凭据填充, 若勾选过"记住密码"则直接发起登录
SharedPreferences sp = getSharedPreferences(AboxConstants.AUTOLOGINFILE, Context.MODE_PRIVATE);
boolean autoFlag = sp.getBoolean(AboxConstants.AutoLoginClass.AUTOLOGIN_FLAG, false);
String savedUser = sp.getString(AboxConstants.AutoLoginClass.AUTOLOGIN_USERNAME, "");
String savedPwd = sp.getString(AboxConstants.AutoLoginClass.AUTOLOGIN_USERPWD, "");
if (!savedUser.isEmpty()) {
usernameEditText.setText(savedUser);
passwordEditText.setText(savedPwd);
checkBoxAutoLogin.setChecked(autoFlag);
// 显式触发表单校验, 确保预填充凭据后登录按钮可用
loginViewModel.loginDataChanged(savedUser, savedPwd);
if (autoFlag && !savedPwd.isEmpty()) {
// 延迟一帧, 等观察者注册完再自动登录
usernameEditText.post(() -> loginViewModel.login(savedUser, savedPwd));
}
}
loginViewModel.getLoginFormState().observe(this, new Observer<LoginFormState>() {
@Override
public void onChanged(@Nullable LoginFormState loginFormState) {
if (loginFormState == null) {
return;
}
loginButton.setEnabled(loginFormState.isDataValid());
if (loginFormState.getUsernameError() != null) {
usernameEditText.setError(getString(loginFormState.getUsernameError()));
}
if (loginFormState.getPasswordError() != null) {
passwordEditText.setError(getString(loginFormState.getPasswordError()));
}
}
});
loginViewModel.getLoginResult().observe(this, new Observer<LoginResult>() {
@Override
public void onChanged(@Nullable LoginResult loginResult) {
if (loginResult == null) {
return;
}
loadingProgressBar.setVisibility(View.GONE);
if (loginResult.getError() != null) {
showLoginFailed(loginResult.getError());
}
if (loginResult.getSuccess() != null) {
// 登录成功: 按勾选状态保存凭据到 SharedPreferences(与 base 行为一致)
String user = usernameEditText.getText().toString();
String pwd = passwordEditText.getText().toString();
boolean remember = checkBoxAutoLogin.isChecked();
SharedPreferences sp = getSharedPreferences(AboxConstants.AUTOLOGINFILE, Context.MODE_PRIVATE);
sp.edit().putString(AboxConstants.AutoLoginClass.AUTOLOGIN_USERNAME, user)
.putString(AboxConstants.AutoLoginClass.AUTOLOGIN_USERPWD, pwd)
.putBoolean(AboxConstants.AutoLoginClass.AUTOLOGIN_FLAG, remember)
.apply();
updateUiWithUser(loginResult.getSuccess());
// 跳转到插座控制页面
Intent intent = new Intent(LoginActivity.this, SwitchControlActivity.class);
startActivity(intent);
finish();
}
}
});
TextWatcher afterTextChangedListener = new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
// ignore
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
// ignore
}
@Override
public void afterTextChanged(Editable s) {
loginViewModel.loginDataChanged(usernameEditText.getText().toString(),
passwordEditText.getText().toString());
}
};
usernameEditText.addTextChangedListener(afterTextChangedListener);
passwordEditText.addTextChangedListener(afterTextChangedListener);
passwordEditText.setOnEditorActionListener(new TextView.OnEditorActionListener() {
@Override
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
if (actionId == EditorInfo.IME_ACTION_DONE) {
loginViewModel.login(usernameEditText.getText().toString(),
passwordEditText.getText().toString());
}
return false;
}
});
loginButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
loadingProgressBar.setVisibility(View.VISIBLE);
loginViewModel.login(usernameEditText.getText().toString(),
passwordEditText.getText().toString());
}
});
}
private void updateUiWithUser(LoggedInUserView model) {
String welcome = getString(R.string.welcome) + model.getDisplayName();
Toast.makeText(getApplicationContext(), welcome, Toast.LENGTH_LONG).show();
}
private void showLoginFailed(String errorString) {
Toast.makeText(getApplicationContext(), errorString, Toast.LENGTH_SHORT).show();
}
}
@@ -0,0 +1,40 @@
package tr.dhee.abox.demo.ui.login;
import androidx.annotation.Nullable;
/**
* Data validation state of the login form.
*/
class LoginFormState {
@Nullable
private Integer usernameError;
@Nullable
private Integer passwordError;
private boolean isDataValid;
LoginFormState(@Nullable Integer usernameError, @Nullable Integer passwordError) {
this.usernameError = usernameError;
this.passwordError = passwordError;
this.isDataValid = false;
}
LoginFormState(boolean isDataValid) {
this.usernameError = null;
this.passwordError = null;
this.isDataValid = isDataValid;
}
@Nullable
Integer getUsernameError() {
return usernameError;
}
@Nullable
Integer getPasswordError() {
return passwordError;
}
boolean isDataValid() {
return isDataValid;
}
}
@@ -0,0 +1,31 @@
package tr.dhee.abox.demo.ui.login;
import androidx.annotation.Nullable;
/**
* Authentication result : success (user details) or error message.
*/
class LoginResult {
@Nullable
private LoggedInUserView success;
@Nullable
private String error;
LoginResult(@Nullable String error) {
this.error = error;
}
LoginResult(@Nullable LoggedInUserView success) {
this.success = success;
}
@Nullable
LoggedInUserView getSuccess() {
return success;
}
@Nullable
String getError() {
return error;
}
}
@@ -0,0 +1,64 @@
package tr.dhee.abox.demo.ui.login;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
import tr.dhee.abox.demo.R;
import tr.dhee.abox.demo.data.LoginRepository;
import tr.dhee.abox.demo.data.Result;
import tr.dhee.abox.demo.data.model.LoggedInUser;
public class LoginViewModel extends ViewModel {
private MutableLiveData<LoginFormState> loginFormState = new MutableLiveData<>();
private MutableLiveData<LoginResult> loginResult = new MutableLiveData<>();
private LoginRepository loginRepository;
LoginViewModel(LoginRepository loginRepository) {
this.loginRepository = loginRepository;
}
LiveData<LoginFormState> getLoginFormState() {
return loginFormState;
}
LiveData<LoginResult> getLoginResult() {
return loginResult;
}
public void login(String username, String password) {
// 后台线程执行 SDK 登录, 结果回到主线程再更新 LiveData
loginRepository.login(username, password, result -> {
if (result instanceof Result.Success) {
LoggedInUser data = ((Result.Success<LoggedInUser>) result).getData();
loginResult.postValue(new LoginResult(new LoggedInUserView(data.getDisplayName())));
} else {
// 取出 LoginDataSource 里封装的中文错误提示
Exception err = ((Result.Error) result).getError();
loginResult.postValue(new LoginResult(err != null ? err.getMessage() : "登录失败"));
}
});
}
public void loginDataChanged(String username, String password) {
if (!isUserNameValid(username)) {
loginFormState.setValue(new LoginFormState(R.string.invalid_username, null));
} else if (!isPasswordValid(password)) {
loginFormState.setValue(new LoginFormState(null, R.string.invalid_password));
} else {
loginFormState.setValue(new LoginFormState(true));
}
}
// 仅校验不为空
private boolean isUserNameValid(String username) {
return username != null && !username.trim().isEmpty();
}
// 仅校验不为空
private boolean isPasswordValid(String password) {
return password != null && !password.trim().isEmpty();
}
}
@@ -0,0 +1,26 @@
package tr.dhee.abox.demo.ui.login;
import androidx.lifecycle.ViewModel;
import androidx.lifecycle.ViewModelProvider;
import androidx.annotation.NonNull;
import tr.dhee.abox.demo.data.LoginDataSource;
import tr.dhee.abox.demo.data.LoginRepository;
/**
* ViewModel provider factory to instantiate LoginViewModel.
* Required given LoginViewModel has a non-empty constructor
*/
public class LoginViewModelFactory implements ViewModelProvider.Factory {
@NonNull
@Override
@SuppressWarnings("unchecked")
public <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
if (modelClass.isAssignableFrom(LoginViewModel.class)) {
return (T) new LoginViewModel(LoginRepository.getInstance(new LoginDataSource()));
} else {
throw new IllegalArgumentException("Unknown ViewModel class");
}
}
}
@@ -0,0 +1,91 @@
package tr.dhee.abox.demo.ui.switchcontrol;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;
import androidx.activity.EdgeToEdge;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.content.ContextCompat;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;
import androidx.lifecycle.ViewModelProvider;
import tr.dhee.abox.demo.databinding.ActivitySwitchControlBinding;
public class SwitchControlActivity extends AppCompatActivity {
private SwitchViewModel switchViewModel;
private ActivitySwitchControlBinding binding;
// 设备名称, 可根据实际需要调整
private static final String DEVICE_NAME = "智能插座";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
EdgeToEdge.enable(this);
binding = ActivitySwitchControlBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
ViewCompat.setOnApplyWindowInsetsListener(binding.main, (v, insets) -> {
Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
return insets;
});
switchViewModel = new ViewModelProvider(this, new SwitchViewModelFactory())
.get(SwitchViewModel.class);
final Button btnOn = binding.btnOn;
final Button btnOff = binding.btnOff;
final ProgressBar loadingProgressBar = binding.loading;
final TextView statusText = binding.statusText;
// 观察控制结果
switchViewModel.getSwitchResult().observe(this, switchResult -> {
if (switchResult == null) {
return;
}
loadingProgressBar.setVisibility(View.GONE);
btnOn.setEnabled(true);
btnOff.setEnabled(true);
if (switchResult.getSuccess() != null) {
statusText.setVisibility(View.VISIBLE);
statusText.setText(switchResult.getSuccess());
statusText.setTextColor(ContextCompat.getColor(SwitchControlActivity.this,
android.R.color.holo_green_dark));
Toast.makeText(this, switchResult.getSuccess(), Toast.LENGTH_SHORT).show();
}
if (switchResult.getError() != null) {
statusText.setVisibility(View.VISIBLE);
statusText.setText(switchResult.getError());
statusText.setTextColor(ContextCompat.getColor(SwitchControlActivity.this,
android.R.color.holo_red_dark));
Toast.makeText(this, switchResult.getError(), Toast.LENGTH_SHORT).show();
}
});
// "开" 按钮
btnOn.setOnClickListener(v -> {
loadingProgressBar.setVisibility(View.VISIBLE);
btnOn.setEnabled(false);
btnOff.setEnabled(false);
switchViewModel.controlSwitch(DEVICE_NAME, "1");
});
// "关" 按钮
btnOff.setOnClickListener(v -> {
loadingProgressBar.setVisibility(View.VISIBLE);
btnOn.setEnabled(false);
btnOff.setEnabled(false);
switchViewModel.controlSwitch(DEVICE_NAME, "0");
});
}
}
@@ -0,0 +1,28 @@
package tr.dhee.abox.demo.ui.switchcontrol;
import androidx.annotation.Nullable;
/**
* Switch control result: success message or error message.
*/
class SwitchResult {
@Nullable
private String success;
@Nullable
private String error;
SwitchResult(@Nullable String success, @Nullable String error) {
this.success = success;
this.error = error;
}
@Nullable
String getSuccess() {
return success;
}
@Nullable
String getError() {
return error;
}
}
@@ -0,0 +1,41 @@
package tr.dhee.abox.demo.ui.switchcontrol;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
import tr.dhee.abox.demo.data.Result;
import tr.dhee.abox.demo.data.SwitchRepository;
public class SwitchViewModel extends ViewModel {
private final MutableLiveData<SwitchResult> switchResult = new MutableLiveData<>();
private final SwitchRepository switchRepository;
SwitchViewModel(SwitchRepository switchRepository) {
this.switchRepository = switchRepository;
}
LiveData<SwitchResult> getSwitchResult() {
return switchResult;
}
/**
* 控制插座开关
*
* @param soDevName 插座设备名称
* @param status 状态: "1"(开) 或 "0"(关)
*/
public void controlSwitch(String soDevName, String status) {
switchRepository.sockCtrl(soDevName, status, result -> {
if (result instanceof Result.Success) {
String data = ((Result.Success<String>) result).getData();
switchResult.postValue(new SwitchResult(data, null));
} else {
Exception err = ((Result.Error) result).getError();
switchResult.postValue(new SwitchResult(null,
err != null ? err.getMessage() : "控制失败"));
}
});
}
}
@@ -0,0 +1,25 @@
package tr.dhee.abox.demo.ui.switchcontrol;
import androidx.lifecycle.ViewModel;
import androidx.lifecycle.ViewModelProvider;
import androidx.annotation.NonNull;
import tr.dhee.abox.demo.data.SwitchDataSource;
import tr.dhee.abox.demo.data.SwitchRepository;
/**
* ViewModel provider factory to instantiate SwitchViewModel.
*/
public class SwitchViewModelFactory implements ViewModelProvider.Factory {
@NonNull
@Override
@SuppressWarnings("unchecked")
public <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
if (modelClass.isAssignableFrom(SwitchViewModel.class)) {
return (T) new SwitchViewModel(SwitchRepository.getInstance(new SwitchDataSource()));
} else {
throw new IllegalArgumentException("Unknown ViewModel class");
}
}
}
@@ -0,0 +1,71 @@
package tr.dhee.abox.demo.utils;
import java.util.HashMap;
/**
* 登录相关常量: SDK 返回码 -> 中文提示; 自动登录 SharedPreferences key。
* 移植自 base 工程的 AboxConstants(仅保留登录部分)。
*/
public class AboxConstants {
public static final HashMap<String, String> codeMap = new HashMap<String, String>();
public static final String AUTOLOGINFILE = "autoLogin";
public static class AutoLoginClass {
public static final String AUTOLOGIN_USERNAME = "userName";
public static final String AUTOLOGIN_USERPWD = "userPwd";
public static final String AUTOLOGIN_FLAG = "flag";
}
static {
codeMap.put("00000", "处理成功");
codeMap.put("00001", "数据库操作失败");
codeMap.put("10001", "TOKEN不存在或长度不足");
codeMap.put("10002", "TOKEN已失效");
codeMap.put("10003", "TOKEN中的SN号不正");
codeMap.put("10004", "签名不存在");
codeMap.put("10005", "时间戳不存在");
codeMap.put("10006", "签名不正确");
codeMap.put("20000", "请求超时");
codeMap.put("20001", "用户名或密码不存在");
codeMap.put("20002", "用户不存在或已暂停使用");
codeMap.put("20003", "登录失败次数过多");
codeMap.put("20004", "用户名或密码不正确");
codeMap.put("20005", "本地用户权限不足");
codeMap.put("20101", "服务器端URL或酒店名或房间号或APIKEY不存在");
codeMap.put("20201", "红外KEY不存在");
codeMap.put("20202", "红外码值不存在");
codeMap.put("20203", "红外设备不存在");
codeMap.put("20204", "红外码库导入失败");
codeMap.put("20205", "红外发射失败");
codeMap.put("20206", "红外设备已离线");
codeMap.put("20301", "参数不正_CMD0不存在");
codeMap.put("20302", "参数不正_CMD1不存在");
codeMap.put("20303", "参数不正_PAYLOAD不存在");
codeMap.put("20304", "窗帘设备不存在");
codeMap.put("20305", "窗帘设备控制失败");
codeMap.put("20306", "窗帘设备已离线");
codeMap.put("20501", "参数不正_插座状态不存在");
codeMap.put("20502", "插座设备不存在");
codeMap.put("20503", "插座设备控制失败");
codeMap.put("20504", "插座设备已离线");
codeMap.put("20601", "参数不正_开关状态不存在");
codeMap.put("20602", "参数不正_开关类型不存在(标识几开开关)");
codeMap.put("20603", "开关设备不存在");
codeMap.put("20604", "开关设备控制失败");
codeMap.put("20605", "开关设备已掉线");
codeMap.put("20701", "参数不正_射灯操作类型不存在");
codeMap.put("20702", "参数不正_射灯操作类型不正确");
codeMap.put("20703", "参数不正_射灯开关状态不存在");
codeMap.put("20704", "参数不正_射灯色调饱和度不存在");
codeMap.put("20705", "参数不正_射灯亮度不存在");
codeMap.put("20706", "射灯设备不存在");
codeMap.put("20707", "射灯设备控制失败");
codeMap.put("20801", "参数不正_设备名称不存在");
codeMap.put("20802", "端末设备不存在");
codeMap.put("20803", "端末状态属性不存在");
codeMap.put("20804", "参数不正_报警时长不正确");
codeMap.put("20805", "端末设备已离线");
codeMap.put("20806", "设备控制失败");
}
}
@@ -0,0 +1,12 @@
# Add project specific R8 rules here.
# AGP will combine all keep rule files in src/main/keepRules to pass to R8
#
# For more details, see
# https://d.android.com/r/tools/r8/keep-rules
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
@@ -0,0 +1,170 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
</vector>
@@ -0,0 +1,30 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="85.84757"
android:endY="92.4963"
android:startX="42.9492"
android:startY="49.59793"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
android:strokeWidth="1"
android:strokeColor="#00000000" />
</vector>
@@ -0,0 +1,67 @@
<?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:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.login.LoginActivity">
<EditText
android:id="@+id/username"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="96dp"
android:autofillHints="@string/prompt_email"
android:hint="@string/prompt_email"
android:inputType="textEmailAddress"
android:selectAllOnFocus="true"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<EditText
android:id="@+id/password"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:autofillHints="@string/prompt_password"
android:hint="@string/prompt_password"
android:imeActionLabel="@string/action_sign_in_short"
android:imeOptions="actionDone"
android:inputType="textPassword"
android:selectAllOnFocus="true"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/username" />
<Button
android:id="@+id/login"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="start"
android:layout_marginTop="16dp"
android:layout_marginBottom="64dp"
android:enabled="false"
android:text="@string/action_sign_in"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/password"
app:layout_constraintVertical_bias="0.2" />
<ProgressBar
android:id="@+id/loading"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginTop="64dp"
android:layout_marginBottom="64dp"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="@+id/password"
app:layout_constraintStart_toStartOf="@+id/password"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.3" />
</androidx.constraintlayout.widget.ConstraintLayout>
@@ -0,0 +1,74 @@
<?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:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.login.LoginActivity">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="840dp"
android:layout_height="match_parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent">
<EditText
android:id="@+id/username"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="96dp"
android:autofillHints="@string/prompt_email"
android:hint="@string/prompt_email"
android:inputType="textEmailAddress"
android:selectAllOnFocus="true"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<EditText
android:id="@+id/password"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:autofillHints="@string/prompt_password"
android:hint="@string/prompt_password"
android:imeActionLabel="@string/action_sign_in_short"
android:imeOptions="actionDone"
android:inputType="textPassword"
android:selectAllOnFocus="true"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/username" />
<Button
android:id="@+id/login"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="start"
android:layout_marginTop="16dp"
android:layout_marginBottom="64dp"
android:enabled="false"
android:text="@string/action_sign_in"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/password"
app:layout_constraintVertical_bias="0.2" />
<ProgressBar
android:id="@+id/loading"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginTop="64dp"
android:layout_marginBottom="64dp"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="@+id/password"
app:layout_constraintStart_toStartOf="@+id/password"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.3" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
@@ -0,0 +1,77 @@
<?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:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.login.LoginActivity">
<EditText
android:id="@+id/username"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="96dp"
android:autofillHints="@string/prompt_email"
android:hint="@string/prompt_email"
android:inputType="textEmailAddress"
android:selectAllOnFocus="true"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<EditText
android:id="@+id/password"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:autofillHints="@string/prompt_password"
android:hint="@string/prompt_password"
android:imeActionLabel="@string/action_sign_in_short"
android:imeOptions="actionDone"
android:inputType="textPassword"
android:selectAllOnFocus="true"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/username" />
<Button
android:id="@+id/login"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="start"
android:layout_marginTop="16dp"
android:layout_marginBottom="8dp"
android:enabled="false"
android:text="@string/action_sign_in"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/password"
app:layout_constraintVertical_bias="0.2" />
<CheckBox
android:id="@+id/checkBoxAutoLogin"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/auto_login"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/login" />
<ProgressBar
android:id="@+id/loading"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginTop="64dp"
android:layout_marginBottom="64dp"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="@+id/password"
app:layout_constraintStart_toStartOf="@+id/password"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.3" />
</androidx.constraintlayout.widget.ConstraintLayout>
@@ -0,0 +1,75 @@
<?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:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.switchcontrol.SwitchControlActivity">
<TextView
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="48dp"
android:text="@string/switch_control_title"
android:textSize="24sp"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/btn_on"
android:layout_width="160dp"
android:layout_height="120dp"
android:layout_marginTop="64dp"
android:text="@string/switch_on"
android:textSize="28sp"
android:textStyle="bold"
app:layout_constraintEnd_toStartOf="@+id/guideline_center"
app:layout_constraintHorizontal_chainStyle="spread"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/title" />
<Button
android:id="@+id/btn_off"
android:layout_width="160dp"
android:layout_height="120dp"
android:text="@string/switch_off"
android:textSize="28sp"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/guideline_center"
app:layout_constraintTop_toTopOf="@+id/btn_on" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline_center"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_percent="0.5" />
<ProgressBar
android:id="@+id/loading"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/btn_on" />
<TextView
android:id="@+id/status_text"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="24dp"
android:gravity="center"
android:textSize="16sp"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/loading" />
</androidx.constraintlayout.widget.ConstraintLayout>
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 982 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

@@ -0,0 +1,3 @@
<resources>
<dimen name="activity_horizontal_margin">48dp</dimen>
</resources>
@@ -0,0 +1,16 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.Aboxtest" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/purple_200</item>
<item name="colorPrimaryVariant">@color/purple_700</item>
<item name="colorOnPrimary">@color/black</item>
<!-- Secondary brand color. -->
<item name="colorSecondary">@color/teal_200</item>
<item name="colorSecondaryVariant">@color/teal_200</item>
<item name="colorOnSecondary">@color/black</item>
<!-- Status bar color. -->
<item name="android:statusBarColor">?attr/colorPrimaryVariant</item>
<!-- Customize your theme here. -->
</style>
</resources>
@@ -0,0 +1,3 @@
<resources>
<dimen name="activity_horizontal_margin">200dp</dimen>
</resources>
@@ -0,0 +1,3 @@
<resources>
<dimen name="activity_horizontal_margin">48dp</dimen>
</resources>
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="purple_200">#FFBB86FC</color>
<color name="purple_500">#FF6200EE</color>
<color name="purple_700">#FF3700B3</color>
<color name="teal_200">#FF03DAC5</color>
<color name="teal_700">#FF018786</color>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
</resources>
@@ -0,0 +1,5 @@
<resources>
<!-- Default screen margins, per the Android Design guidelines. -->
<dimen name="activity_horizontal_margin">16dp</dimen>
<dimen name="activity_vertical_margin">16dp</dimen>
</resources>
@@ -0,0 +1,16 @@
<resources>
<string name="app_name">login</string>
<!-- Strings related to login -->
<string name="prompt_email">Email</string>
<string name="prompt_password">Password</string>
<string name="action_sign_in">Sign in or register</string>
<string name="action_sign_in_short">Sign in</string>
<string name="welcome">"Welcome !"</string>
<string name="invalid_username">用户名不能为空</string>
<string name="invalid_password">密码不能为空</string>
<string name="login_failed">"Login failed"</string>
<string name="auto_login">记住密码</string>
<string name="switch_control_title">插座控制</string>
<string name="switch_on"></string>
<string name="switch_off"></string>
</resources>
@@ -0,0 +1,16 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.Aboxtest" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/purple_500</item>
<item name="colorPrimaryVariant">@color/purple_700</item>
<item name="colorOnPrimary">@color/white</item>
<!-- Secondary brand color. -->
<item name="colorSecondary">@color/teal_200</item>
<item name="colorSecondaryVariant">@color/teal_700</item>
<item name="colorOnSecondary">@color/black</item>
<!-- Status bar color. -->
<item name="android:statusBarColor">?attr/colorPrimaryVariant</item>
<!-- Customize your theme here. -->
</style>
</resources>
@@ -0,0 +1,17 @@
package tr.dhee.abox.demo;
import org.junit.Test;
import static org.junit.Assert.*;
/**
* Example local unit test, which will execute on the development machine (host).
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
public class ExampleUnitTest {
@Test
public void addition_isCorrect() {
assertEquals(4, 2 + 2);
}
}