docs: archive ABSDK Android sample
@@ -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>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 982 B |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 5.8 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
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);
|
||||
}
|
||||
}
|
||||