diff --git a/app/build.gradle b/app/build.gradle index 25c07ef..d83b116 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -75,6 +75,8 @@ dependencies { // implementation files('libs/ksoap2-android-assembly-3.6.0-jar-with-dependencies.jar') implementation files('libs/badgeview.jar') implementation files('libs/TbsFileSdk_base_arm64_release_1.0.5.6000030.20231109143447.aar') + //深信服零信任SDK + implementation files('libs/SangforSDK.aar') implementation libs.androidx.core.ktx diff --git a/app/libs/SangforSDK.aar b/app/libs/SangforSDK.aar new file mode 100644 index 0000000..c4a5324 Binary files /dev/null and b/app/libs/SangforSDK.aar differ diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index f643103..59f96ac 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -81,6 +81,12 @@ tools:ignore="GoogleAppIndexingWarning" tools:replace="android:allowBackup" tools:targetApi="s"> + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/rehome/zhdcoa/App.java b/app/src/main/java/com/rehome/zhdcoa/App.java index c698adf..3d74391 100644 --- a/app/src/main/java/com/rehome/zhdcoa/App.java +++ b/app/src/main/java/com/rehome/zhdcoa/App.java @@ -1,20 +1,16 @@ package com.rehome.zhdcoa; -import android.app.ActivityManager; -import android.content.ComponentName; import android.content.Context; -import android.content.Intent; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.os.Build; -import android.os.Process; import android.text.TextUtils; import android.util.Log; +import android.widget.Toast; import androidx.annotation.Nullable; import androidx.emoji.bundled.BundledEmojiCompatConfig; import androidx.emoji.text.EmojiCompat; -import androidx.multidex.BuildConfig; import androidx.multidex.MultiDex; //import com.blankj.utilcode.util.Utils; @@ -25,9 +21,18 @@ import com.github.mikephil.charting.utils.Utils; //import com.hjq.http.model.HttpHeaders; //import com.hjq.http.model.HttpParams; import com.rehome.zhdcoa.bean.UserInfoBean; +import com.rehome.zhdcoa.ui.activity.VpnAuthActivity; +import com.rehome.zhdcoa.utils.DpPxSpTransformUtil; import com.rehome.zhdcoa.utils.SPUtils; //import com.squareup.leakcanary.LeakCanary; //import com.squareup.leakcanary.RefWatcher; +import com.rehome.zhdcoa.vpn.GlobalListenerManager; +import com.sangfor.sdk.SFUemSDK; +import com.sangfor.sdk.base.SFBaseMessage; +import com.sangfor.sdk.base.SFSDKFlags; +import com.sangfor.sdk.base.SFSDKMode; +import com.sangfor.sdk.base.SFSetSpaConfigListener; +import com.sangfor.sdk.utils.SFLogN; import com.tencent.bugly.crashreport.CrashReport; import com.xuexiang.xui.XUI; import com.yolanda.nohttp.NoHttp; @@ -35,11 +40,6 @@ import com.zhy.autolayout.config.AutoLayoutConifg; import org.litepal.LitePalApplication; -import java.util.HashMap; -import java.util.List; - -import okhttp3.OkHttpClient; - public class App extends LitePalApplication { private UserInfoBean.UserInfo userInfo; @@ -60,6 +60,8 @@ public class App extends LitePalApplication { private int activityAount = 0; private boolean isForeground = false; + public static Context mContext; + public static App getInstance() { return mInstance; @@ -152,6 +154,37 @@ public class App extends LitePalApplication { protected void attachBaseContext(Context base) { super.attachBaseContext(base); MultiDex.install(this); + mContext = base; + + /** + * 初始化深信服sdk,推荐在attachBaseContext中调用,因为sdk延后初始化会导致多进程场景下,子进程无法拥有sdk的能力 + * 初始化深信服注销监听回调 + * 初始化深信服dp,sp转px的工具类 + */ + if (isApkInDebug(base)) { + showLog("attachBaseContext base"); + initSdk(base); + GlobalListenerManager.getInstance().init(base); + DpPxSpTransformUtil.init(getResources().getDisplayMetrics().density); +// String config = "{\"loginAddress\":\"https://vpn.zhp.geg.com.cn:55420\", \"spaSecret\":\"7FFQ-rWE5-V6xX\"}"; +// SFUemSDK.setSpaConfig(config, new SFSetSpaConfigListener() { +// @Override +// public void onSetSpaConfig(String result, SFBaseMessage error) { +// SFLogN.info("app", "spa result:"+ result + ", error:" + error); +// Log.i("app", "spa result:"+ result + ", error:" + error); +// if (error.mErrCode != 0) { +// Toast.makeText(mContext, +// "SPA设置失败"+ ", Error Message:" + error.toString(), +// Toast.LENGTH_SHORT) +// .show(); +// } else { +// Toast.makeText(mContext, "SPA设置成功, result:" + result, +// Toast.LENGTH_SHORT) +// .show(); +// } +// } +// }); + } } public void clearUser() { @@ -211,5 +244,45 @@ public class App extends LitePalApplication { } } + public void showLog(String logText) { + if (isApkInDebug(mContext)) { + if (TextUtils.isEmpty(logText)) { + Log.i("app", "logText is null"); + } else { + Log.i("app", logText); + } + } + } + void initSdk(Context context) { + SFSDKMode sdkMode = SFSDKMode.MODE_SUPPORT_MUTABLE; // 表明启用可变授权功能,详情参考集成指导文档 + switch (sdkMode) { + case MODE_SUPPORT_MUTABLE:{ + int sdkFlags = SFSDKFlags.FLAGS_HOST_APPLICATION; //表明是单应用或者是主应用 + sdkFlags |= SFSDKFlags.FLAGS_VPN_MODE_TCP; //表明使用VPN功能中的TCP模式 + SFUemSDK.getInstance().initSDK(this, sdkMode,sdkFlags,null); + break; + } + case MODE_VPN: { //只使用VPN功能场景 + int sdkFlags = SFSDKFlags.FLAGS_HOST_APPLICATION; //表明是单应用或者是主应用 + sdkFlags |= SFSDKFlags.FLAGS_VPN_MODE_TCP; //表明使用VPN功能中的TCP模式 + SFUemSDK.getInstance().initSDK(this, sdkMode,sdkFlags,null); + break; + } + case MODE_SANDBOX: { //只使用安全沙箱功能场景 + int sdkFlags = SFSDKFlags.FLAGS_HOST_APPLICATION; //表明是单应用或者是主应用 + SFUemSDK.getInstance().initSDK(this, sdkMode,sdkFlags,null); + break; + } + case MODE_VPN_SANDBOX: { //同时使用VPN功能+安全沙箱功能场景 + int sdkFlags = SFSDKFlags.FLAGS_HOST_APPLICATION; //表明是单应用或者是主应用 + sdkFlags |= SFSDKFlags.FLAGS_VPN_MODE_TCP; //表明使用VPN功能中的TCP模式 + SFUemSDK.getInstance().initSDK(this, sdkMode,sdkFlags,null); + break; + } + default: { + Toast.makeText(context, "SDK模式错误", Toast.LENGTH_LONG).show(); + } + } + } } diff --git a/app/src/main/java/com/rehome/zhdcoa/Contans.java b/app/src/main/java/com/rehome/zhdcoa/Contans.java index 111d5f0..8ab7b1d 100644 --- a/app/src/main/java/com/rehome/zhdcoa/Contans.java +++ b/app/src/main/java/com/rehome/zhdcoa/Contans.java @@ -93,6 +93,9 @@ public class Contans { /********************广播字段(唯一性)********************************/ public static final String ACTION_REFLASH_PRODUCE_FRAGMENT = "ACTION_REFLASH_PRODUCE_FRAGMENT"; + public static final String USER_NAME_VPN = "usernameVpn";//vpn用户名 + public static final String PASS_WORD_VPN = "pwdVpn";//vpn密码 + public class KEY { public static final String SELECT_PERSON = "select_person";//选择的人数 public static final String SELECT_ADDR = "select_addr";//选择的乘车地址 diff --git a/app/src/main/java/com/rehome/zhdcoa/ui/activity/AuthSuccessActivity.java b/app/src/main/java/com/rehome/zhdcoa/ui/activity/AuthSuccessActivity.java new file mode 100644 index 0000000..a75effc --- /dev/null +++ b/app/src/main/java/com/rehome/zhdcoa/ui/activity/AuthSuccessActivity.java @@ -0,0 +1,679 @@ +package com.rehome.zhdcoa.ui.activity; + + +import android.annotation.SuppressLint; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.graphics.Bitmap; +import android.net.http.SslError; +import android.os.AsyncTask; +import android.os.Bundle; +import android.os.SystemClock; +import android.text.TextUtils; +import android.util.Log; +import android.view.View; +import android.view.Window; +import android.webkit.DownloadListener; +import android.webkit.SslErrorHandler; +import android.webkit.WebSettings; +import android.webkit.WebView; +import android.webkit.WebViewClient; +import android.widget.ArrayAdapter; +import android.widget.AutoCompleteTextView; +import android.widget.Button; +import android.widget.EditText; +import android.widget.FrameLayout; +import android.widget.RadioButton; +import android.widget.RadioGroup; +import android.widget.Toast; + +import androidx.appcompat.app.AppCompatActivity; + +import com.sangfor.sdk.SFUemSDK; +import com.sangfor.sdk.base.SFBaseMessage; +import com.sangfor.sdk.base.SFTunnelStatus; +import com.sangfor.sdk.base.SFTunnelStatusListener; +import com.sangfor.sdk.utils.SFLogN; +//import com.sangfor.sdkdemo.BuildConfig; +import com.rehome.zhdcoa.R; +//import com.sangfor.sdkdemo.primary.L3VPNPrimaryAuthActivity; +import com.rehome.zhdcoa.vpn.SignatureUtils; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.lang.ref.WeakReference; +import java.net.InetSocketAddress; +import java.net.Proxy; +import java.net.URL; +import java.net.URLConnection; +import java.net.URLDecoder; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.security.cert.X509Certificate; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; + +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; + +public class AuthSuccessActivity extends AppCompatActivity implements View.OnClickListener, SFTunnelStatusListener { + + private static final String TAG = "app"; + private static String sTestURL = "https://mobile.zhp.geg.com.cn:7082/ApkUpdate/GetData.aspx"; + private final int TEST_URL_TIMEOUT_MILLIS = 8 * 1000;// 测试vpn资源的超时时间 + private EditText mEtUrl; + private FrameLayout mWebViewContainer = null; + private WebView mWebView = null; + private RadioGroup mRadioGroup_authMethod = null; + private RadioButton mRadioButton_selected_authMethod = null; + private AutoCompleteTextView mAutoCompleteTextView = null; + + //L3VPN全局vpn模式才需要关注的处理逻辑 + private boolean mIsFromL3vpn = false; + + private static ThreadPoolExecutor sBackupExecutor = + new ThreadPoolExecutor(3, 3, 1, TimeUnit.SECONDS, new LinkedBlockingQueue(), new ThreadFactory() { + private final AtomicInteger mCount = new AtomicInteger(1); + + @Override + public Thread newThread(Runnable r) { + return new Thread(r, "downloadfile thread #" + mCount.getAndIncrement()); + } + }); + + static { + sBackupExecutor.allowCoreThreadTimeOut(true); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + Log.i(TAG, "onCreate..."); + super.onCreate(savedInstanceState); + requestWindowFeature(Window.FEATURE_NO_TITLE); + setContentView(R.layout.activity_auth_success); + + initView(); + initClickEvents(); + } + + private void initView() { + mEtUrl = findViewById(R.id.et_url); + mRadioGroup_authMethod = findViewById(R.id.svpn_resource_tabheader); + mAutoCompleteTextView = findViewById(R.id.autoComTextView_url); + mWebViewContainer = findViewById(R.id.web_view_container); + mWebView = new WebView(getApplicationContext()); + mWebViewContainer.addView(mWebView); + setWebViewSettings(); //设置webview配置参数 + + String[] str_extraResource_url = getResources().getStringArray(R.array.extra_resource_url); + ArrayAdapter arrayAdapter = + new ArrayAdapter(AuthSuccessActivity.this, R.layout.simple_spinner_item, + str_extraResource_url); + mAutoCompleteTextView.setAdapter(arrayAdapter); + + mEtUrl.setText(sTestURL); + + + + Intent intent = getIntent(); + mIsFromL3vpn = true; + Button startTunnelBtn = findViewById(R.id.btn_start_tunnel); + Button stopTunnelBtn = findViewById(R.id.btn_stop_tunnel); + //L3VPN模式才显示隧道启动停止按钮和注册隧道状态回调 + if (mIsFromL3vpn) { + startTunnelBtn.setVisibility(View.VISIBLE); + stopTunnelBtn.setVisibility(View.VISIBLE); + startTunnelBtn.setOnClickListener(this); + stopTunnelBtn.setOnClickListener(this); + SFUemSDK.getInstance().getSFTunnel().registerTunnelStatusListener(this); + } else { + startTunnelBtn.setVisibility(View.INVISIBLE); + stopTunnelBtn.setVisibility(View.INVISIBLE); + } + + } + + //注册监听事件 + private void initClickEvents() { + findViewById(R.id.btn_file).setOnClickListener(this); + findViewById(R.id.btn_log).setOnClickListener(this); + findViewById(R.id.btn_master_slave).setOnClickListener(this); + findViewById(R.id.btn_modify_psw).setOnClickListener(this); + + //资源展示按钮变动监听 + mRadioGroup_authMethod.setOnCheckedChangeListener(new RadioGroup.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(RadioGroup group, int checkedId) { + //监听认证按钮,动态改变布局显示 + if(mRadioGroup_authMethod.getCheckedRadioButtonId()==R.id.svpn_intraResource_tabheader){ + findViewById(R.id.et_url).setVisibility(View.VISIBLE); + findViewById(R.id.autoComTextView_url).setVisibility(View.GONE); + } + if(mRadioGroup_authMethod.getCheckedRadioButtonId()==R.id.svpn_extraResource_tabheader){ + findViewById(R.id.et_url).setVisibility(View.GONE); + findViewById(R.id.autoComTextView_url).setVisibility(View.VISIBLE); + } +// switch (mRadioGroup_authMethod.getCheckedRadioButtonId()) { +// case R.id.svpn_intraResource_tabheader: +// findViewById(R.id.et_url).setVisibility(View.VISIBLE); +// findViewById(R.id.autoComTextView_url).setVisibility(View.GONE); +// break; +// case R.id.svpn_extraResource_tabheader: +// findViewById(R.id.et_url).setVisibility(View.GONE); +// findViewById(R.id.autoComTextView_url).setVisibility(View.VISIBLE); +// break; +// default: +// break; +// } + + } + }); + + //AutoCompleteTextView控件获取焦点时,展示提示资源 + mAutoCompleteTextView.setOnFocusChangeListener(new View.OnFocusChangeListener() { + @Override + public void onFocusChange(View v, boolean hasFocus) { + if (hasFocus) { + ((AutoCompleteTextView) v).showDropDown(); + } + } + }); + + + //内部测试代码,不需要,可以删除 + Intent intent = getIntent(); + if (intent != null && intent.getBooleanExtra("inner_test",false)) { + findViewById(R.id.btn_udp).setOnClickListener(this); + } else { + findViewById(R.id.btn_udp).setVisibility(View.GONE); + } + + if (intent != null && intent.getBooleanExtra("inner_test",false)) { + findViewById(R.id.btn_atrust_auth).setOnClickListener(this); + } else { + findViewById(R.id.btn_atrust_auth).setVisibility(View.GONE); + } + + + } + + @Override + public void onClick(View v) { + if(v.getId()==R.id.btn_test_res){ + doTestResource(); + } + if(v.getId()==R.id.btn_logout){ + doVPNLogout(); + } + if(v.getId()==R.id.btn_file){ + entryFileTestPage(); + } + if(v.getId()==R.id.btn_udp){ + entryUdpPage(); + } + if(v.getId()==R.id.btn_atrust_auth){ + entryAtrustAuthPage(); + } + if(v.getId()==R.id.btn_log){ + entryLogTestPage(); + } + if(v.getId()==R.id.btn_start_tunnel){ + SFUemSDK.getInstance().getSFTunnel().startTunnel(); + showLog(String.valueOf(SFUemSDK.getInstance().getAuthStatus())); + } + if(v.getId()==R.id.btn_stop_tunnel){ + SFUemSDK.getInstance().getSFTunnel().stopTunnel(); + } + if(v.getId()==R.id.btn_master_slave){ + //startActivity(new Intent(this, MasterSlaveTestActivity.class)); + } + if(v.getId()==R.id.btn_modify_psw){ + if (!SFUemSDK.getInstance().allowResetPassword()) { + Toast.makeText(AuthSuccessActivity.this, "当前不支持修改密码", Toast.LENGTH_SHORT).show(); + return; + } + //startActivity(new Intent(this, ModifyPswActivity.class)); + } + } + + private void entryUdpPage() { + //内部测试代码,不需要可以删除 + + } + + private void entryAtrustAuthPage() { + //startActivity(new Intent(this, ThirdCodeAuthInnerTestActivity.class)); + } + + private void entryLogTestPage() { + //startActivity(new Intent(this, LogTestActivity.class)); + } + + private void entryFileTestPage() { + //startActivity(new Intent(this, FileTestActivtiy.class)); + } + + private void doTestProxyDomain() { + // 获取选定的认证方式 + mRadioButton_selected_authMethod = (RadioButton) findViewById(mRadioGroup_authMethod.getCheckedRadioButtonId()); + String resourceType = mRadioButton_selected_authMethod.getText().toString().trim(); + final EditText editText = mEtUrl; + + int port = (int) SFUemSDK.getInstance().getSocks5ProxyPort(); + SFLogN.info(TAG, "Access the domain " + editText.getText().toString() + " through port " + port + " via a proxy"); + showLog( "Access the domain " + editText.getText().toString() + " through port " + port + " via a proxy"); + + // 设置代理 + Proxy proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress("127.0.0.1", port)); + + /** + * 测试用的很多域名证书校验通不过,进行自校验 + * 创建自定义的 TrustManager + */ + TrustManager[] trustAllCerts = new TrustManager[]{ + new X509TrustManager() { + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType) { + // 不进行客户端证书校验 + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType) { + // 不进行服务器证书校验 + } + + @Override + public X509Certificate[] getAcceptedIssuers() { + return new X509Certificate[0]; + } + } + }; + + // 创建 SSLContext + SSLContext sslContext; + try { + sslContext = SSLContext.getInstance("SSL"); + sslContext.init(null, trustAllCerts, new java.security.SecureRandom()); + } catch (Exception e) { + SFLogN.error2(TAG, "Failed to create SSL context: ", e.toString()); + return; + } + + OkHttpClient client = new OkHttpClient.Builder() + .proxy(proxy) + .sslSocketFactory(sslContext.getSocketFactory(), (X509TrustManager) trustAllCerts[0]) + .hostnameVerifier((hostname, session) -> true) // 关闭主机名验证 + .build(); + + // 使用 AsyncTask 执行网络请求 + new ProxyAsyncTask(this, client, editText.getText().toString()).execute(); + } + + private static class ProxyAsyncTask extends AsyncTask { + private WeakReference activityReference; + private OkHttpClient client; + private String url; + + ProxyAsyncTask(AuthSuccessActivity context, OkHttpClient client, String url) { + activityReference = new WeakReference<>(context); + this.client = client; + this.url = url; + } + + @Override + protected String doInBackground(Void... voids) { + // 创建请求对象 + Request request = new Request.Builder() + .url(url) + .header("Proxy-Connection", "Keep-Alive") + .build(); + + // 发送请求并获取响应 + Response response = null; + try { + response = client.newCall(request).execute(); + if (response.isSuccessful()) { + return response.body().string(); // 获取响应体内容 + } else { + SFLogN.info(TAG, "The request was failed, and the response code is " + response.code()); + return null; + } + } catch (IOException e) { + SFLogN.error2(TAG, "An exception occurred, and the exception message is: ", e.toString()); + if (response != null) { + response.body().close(); + } + return null; + } + } + + @Override + protected void onPostExecute(String responseBody) { + AuthSuccessActivity activity = activityReference.get(); + if (activity == null || activity.isFinishing()) { + return; // Activity 已经被销毁,直接返回 + } + + if (responseBody != null) { + SFLogN.info(TAG, "The request " + url + " was successful."); + activity.mWebView.loadDataWithBaseURL(url, responseBody, "text/html", "utf-8", null); + } else { + SFLogN.info(TAG, "The request " + url + " was failed."); + } + } + } + + /** + * 测试资源 + */ + private void doTestResource() { + //获取选定的认证方式 + mRadioButton_selected_authMethod = (RadioButton) findViewById(mRadioGroup_authMethod.getCheckedRadioButtonId()); + String resourceType = mRadioButton_selected_authMethod.getText().toString().trim(); + if(resourceType.equals(getString(R.string.str_proxy_domain))) { + SFLogN.info(TAG, "begin to test proxy domain"); + doTestProxyDomain(); + return; + } + final EditText editText = + resourceType.equals(getString(R.string.str_intranet_resource)) ? mEtUrl : mAutoCompleteTextView; + LoadPageByWebView(editText.getText().toString()); + } + + /** + * 注销流程 + */ + private void doVPNLogout() { + // 注销VPN登录. + SFUemSDK.getInstance().logout(); + Toast.makeText(AuthSuccessActivity.this, R.string.str_vpn_logout, Toast.LENGTH_SHORT).show(); + } + + @SuppressLint("SetJavaScriptEnabled") + public void LoadPageByWebView(String url) { + if (url == null || url.equals("")) { + SFLogN.info(TAG, "LoadPageByWebView url is wrong!"); + return; + } + if (!url.contains("http")) { + url = "http://" + url; + } + + showLog(url); + mWebView.loadUrl(url); + } + + /** + * 获取应用签名,用于设置签名白名单,只有签名白名单内的子应用才能正常和主应用通信 + * + * @param packageName + * @return + */ + private String getSafeAppSignature(String packageName) { + PackageManager packageManager = getPackageManager(); + ApplicationInfo applicationInfo; + + try { + applicationInfo = packageManager.getApplicationInfo(packageName, 0); + } catch (PackageManager.NameNotFoundException e) { + SFLogN.warn2(TAG, "getSafeAppSignature failed", "application not found: " + packageName, e); + return ""; + } + String pubkeyInfo = SignatureUtils.getPubKeyInfo(applicationInfo, this); + if (TextUtils.isEmpty(pubkeyInfo)) { + SFLogN.warn2(TAG, "getSafeAppSignature failed", "get pubkeyInfo failed: " + packageName); + return ""; + } + SFLogN.info(TAG, "pubKeyInfo: " + pubkeyInfo + ",packageName: " + packageName); + return pubkeyInfo; + } + + private boolean downloadFile(String url, File dir) { + try { + + SFLogN.info(TAG, "run startDownload"); + long start = SystemClock.elapsedRealtime(); + + URL url1 = new URL(url); + String decodeUrl = URLDecoder.decode(url1.getFile(), "utf-8"); + SFLogN.info(TAG, "downloadFile decodeUrl:" + decodeUrl); + File dstFile = new File(dir, decodeUrl); + try { + if (!dstFile.exists()) { + dstFile.getParentFile().mkdirs(); + dstFile.createNewFile(); + } + } catch (Exception e) { + SFLogN.error(TAG, "downloadFile failed.", e); + } + + SFLogN.info(TAG, "downloadFile dstFile:" + dstFile.getAbsolutePath() + " exist:" + dstFile.exists()); + + URLConnection connection = url1.openConnection(); + connection.connect(); + long contentLen = 0; + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) { + contentLen = connection.getContentLengthLong(); + } else { + contentLen = connection.getContentLength(); + } + SFLogN.info(TAG, "downloadFile content len:" + contentLen); + + InputStream inputStream = connection.getInputStream(); + OutputStream outputStream = new FileOutputStream(dstFile); + byte[] buf = new byte[2048]; + int len = 0; + long totlen = 0; + long percent = 1; + long lastCount = 0, lastTime = SystemClock.elapsedRealtime(); + while ((len = inputStream.read(buf)) >= 0) { + totlen += len; + outputStream.write(buf, 0, len); + if (contentLen > 0 && totlen * 100 > contentLen * percent) { + percent++; + SFLogN.info(TAG, "downloadFile %d%% speed: %.2fMB/s", + (totlen * 100 / contentLen), + (totlen - lastCount) / 1024.0 / 1024 * 1000 / (SystemClock.elapsedRealtime() - lastTime)); + lastCount = totlen; + lastTime = SystemClock.elapsedRealtime(); + } + } + inputStream.close(); + outputStream.close(); + + SFLogN.info(TAG, "downloadFile over,totlen:" + totlen); + + long cost = SystemClock.elapsedRealtime() - start; + runOnUiThread(new Runnable() { + @Override + public void run() { + Toast.makeText(AuthSuccessActivity.this, "下載完成!", Toast.LENGTH_SHORT).show(); + } + }); + SFLogN.info(TAG, "downloadFile cost:%.2fs, speed:%.2fMB/s", (cost / 1000.0f), + (totlen / 1024.0 / 1024.0 / cost * 1000)); + return true; + } catch (Exception e) { + SFLogN.error(TAG, "downloadFile failed.", e); + } + return false; + } + + private void setWebViewSettings() { + mWebView.setWebViewClient(new MyWebViewClient()); + + WebSettings webSettings = mWebView.getSettings(); + // 不使用缓存,只从网络获取数据。 + webSettings.setCacheMode(WebSettings.LOAD_NO_CACHE); + // 开启 DOM storage API 功能 + webSettings.setDomStorageEnabled(true); + // 开启 database storage API 功能 + webSettings.setDatabaseEnabled(true); + // 设置可以支持缩放 + webSettings.setSupportZoom(true); + // 设置出现缩放工具 + webSettings.setBuiltInZoomControls(true); + // 设置可在大视野范围内上下左右拖动,并且可以任意比例缩放 + webSettings.setUseWideViewPort(true); + // 设置默认加载的可视范围是大视野范围 + webSettings.setLoadWithOverviewMode(true); + // 网页中包含JavaScript内容需调用以下方法,参数为true + webSettings.setJavaScriptEnabled(true); + mWebView.setDownloadListener(new DownloadListener() { + @Override + public void onDownloadStart(final String url, String userAgent, String contentDisposition, String mimetype, + long contentLength) { + SFLogN.info(TAG, "onDownloadStart url:" + url + " len:" + contentLength); + SFLogN.info(TAG, "onDownloadStart download dir:" + getExternalFilesDir(null).getAbsolutePath()); + Toast.makeText(AuthSuccessActivity.this, String.format("下载地址Url:%s 下载文件路径:%s", + url, getExternalFilesDir(null).getAbsolutePath()), Toast.LENGTH_LONG).show(); + sBackupExecutor.submit(new Runnable() { + @Override + public void run() { + SFLogN.info(TAG, "run startDownload"); + boolean ret = downloadFile(url, new File(getExternalFilesDir(null) + "/download/")); + SFLogN.info(TAG, "run downloadFile ret:" + ret); + } + }); + } + }); + } + + private class MyWebViewClient extends WebViewClient { + + public MyWebViewClient() { + } + + @Override + // 覆盖WebView默认使用第三方或系统默认浏览器打开网页的行为,使网页用WebView打开 + public boolean shouldOverrideUrlLoading(WebView view, String url) { + view.loadUrl(url); + return true;// 返回值是true的时候控制去WebView打开,为false调用系统浏览器或第三方浏览器 + } + + @Override + public void onPageStarted(WebView view, String url, Bitmap favicon) { + super.onPageStarted(view, url, favicon); + SFLogN.info(TAG, "onPageStarted url = " + url); + + } + + @Override + public void onPageFinished(WebView view, String url) { + //清除缓存 + mWebView.clearCache(true); + mWebView.clearHistory(); + super.onPageFinished(view, url); + } + + @Override + public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) { + handler.proceed();// 忽略证书错误 + } + } + + /** + * WebView的回收销毁,防止内存泄漏 + */ + private void destroyWebView() { + mWebViewContainer.removeAllViews(); + if (mWebView != null) { + mWebView.clearHistory(); + mWebView.clearCache(true); + mWebView.loadUrl("about:blank"); + mWebView.freeMemory(); +// mWebView.pauseTimers(); + mWebView.destroy(); + } + } + + @Override + protected void onResume() { + Log.i(TAG, "onResume..."); + super.onResume(); + } + + @Override + protected void onNewIntent(Intent intent) { + Log.i(TAG, "onNewIntent..."); + super.onNewIntent(intent); + } + + @Override + protected void onDestroy() { + destroyWebView(); + if (mIsFromL3vpn) { + SFUemSDK.getInstance().getSFTunnel().unRegisterTunnelStatusListener(this); + } + super.onDestroy(); + } + + @Override + public void onTunnelStatusChanged(SFTunnelStatus curStatus) { + SFLogN.info(TAG, "onTunnelStatusChanged called: curStatus: " + curStatus); + } + + @Override + public void onTunnelStarted(SFBaseMessage msg) { + SFLogN.info(TAG, "onTunnelStarted, msg: " + msg.mErrStr + ",errCode: " + msg.mErrCode); + + if (msg.mErrCode != 0) { + SFLogN.warn2(TAG, "start Tunnel failed", msg.mErrStr); + showToast("L3VPN隧道启动失败: " + msg.mErrStr); + return; + } + SFLogN.info(TAG, "startTunnel finished"); + showToast("L3VPN隧道启动成功"); + } + + @Override + public void onTunnelStoped() { + SFLogN.info(TAG, "onTunnelStoped"); + showToast("隧道已经关闭"); + } + + + //封装Toast + public void showToast(final String text) { + runOnUiThread(new Runnable() { + @Override + public void run() { + Toast.makeText(AuthSuccessActivity.this, text, Toast.LENGTH_SHORT).show(); + } + }); + } + + public void showLog(String logText) { + if (isApkInDebug(AuthSuccessActivity.this)) { + if(TextUtils.isEmpty(logText)){ + Log.i("app", "logText is null"); + }else{ + Log.i("app", logText); + } + } + } + + /** + * 判断当前应用是否是debug状态 + */ + + public static boolean isApkInDebug(Context context) { + try { + ApplicationInfo info = context.getApplicationInfo(); + return (info.flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0; + } catch (Exception e) { + return false; + } + } +} diff --git a/app/src/main/java/com/rehome/zhdcoa/ui/activity/LoginActivity.kt b/app/src/main/java/com/rehome/zhdcoa/ui/activity/LoginActivity.kt index 85544bd..d8708cb 100644 --- a/app/src/main/java/com/rehome/zhdcoa/ui/activity/LoginActivity.kt +++ b/app/src/main/java/com/rehome/zhdcoa/ui/activity/LoginActivity.kt @@ -18,6 +18,8 @@ import android.view.View import android.widget.CompoundButton import android.widget.RadioGroup import android.widget.Toast +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.contract.ActivityResultContracts import com.google.gson.Gson import com.rehome.zhdcoa.App @@ -29,6 +31,7 @@ import com.rehome.zhdcoa.bean.UserInfoBean import com.rehome.zhdcoa.databinding.ActivityLoginBinding import com.rehome.zhdcoa.ui.toastview.toastviewbymyself import com.rehome.zhdcoa.utils.* +import com.sangfor.sdk.SFUemSDK import com.tencent.bugly.crashreport.CrashReport import com.yolanda.nohttp.NoHttp import com.yolanda.nohttp.RequestMethod @@ -43,6 +46,10 @@ class LoginActivity : BaseActivityOaToolbarViewBinding() { private lateinit var networkCheckType: String + private var usernameVpn = "" + private var pwdVpn = "" + private lateinit var launcherResultVpn: ActivityResultLauncher + override fun getViewBinding() = ActivityLoginBinding.inflate(layoutInflater) override fun getToolbar() = binding.toolbarView.toolbar @@ -50,6 +57,7 @@ class LoginActivity : BaseActivityOaToolbarViewBinding() { override fun initView() { StatusBarUtil.transparencyBar(this) + launcherResultVpn = createActivityResultLauncherVpn() val wifi = SPUtils.get(context, Contans.YX_WIFI_IP, "") as String val mob = SPUtils.get(context, Contans.YX_4G_IP, "") as String if (wifi == "" && mob == "") { @@ -101,6 +109,15 @@ class LoginActivity : BaseActivityOaToolbarViewBinding() { Contans.BASE_URL_AI_3D_SERVER = Contans.BASE_URL_AI_3D_SERVER_EXTRANET //AI三维可视化安防平台登录 电厂外网 } + if (networkCheckType == "aTrust") { + binding.rg.check(R.id.rb3) + Contans.IP = Contans.IP_INTRANET //运行IP内网 + Contans.BASE_URL = Contans.BASE_URL_INTRANET //行政IP内网 + Contans.BASE_URL_COMPANY_SERVER = + Contans.BASE_URL_COMPANY_SERVER_INTRANET //智慧安防平台登录 电厂内网 + Contans.BASE_URL_AI_3D_SERVER = + Contans.BASE_URL_AI_3D_SERVER_INTRANET //AI三维可视化安防平台登录 电厂外网 + } } else { binding.rg.check(R.id.rb2) SPUtils.put(this, Contans.NETWORK_CHECK_TYPE, "外网") @@ -133,6 +150,16 @@ class LoginActivity : BaseActivityOaToolbarViewBinding() { Contans.BASE_URL_AI_3D_SERVER = Contans.BASE_URL_AI_3D_SERVER_EXTRANET //AI三维可视化安防平台登录 电厂外网 } + + R.id.rb3 -> { + SPUtils.put(this, Contans.NETWORK_CHECK_TYPE, "aTrust") + Contans.IP = Contans.IP_INTRANET //运行IP内网 + Contans.BASE_URL = Contans.BASE_URL_INTRANET //行政IP内网 + Contans.BASE_URL_COMPANY_SERVER = + Contans.BASE_URL_COMPANY_SERVER_INTRANET //智慧安防平台登录 电厂内网 + Contans.BASE_URL_AI_3D_SERVER = + Contans.BASE_URL_AI_3D_SERVER_INTRANET //AI三维可视化安防平台登录 电厂外网 + } } }) binding.cbAgreement.setOnCheckedChangeListener(CompoundButton.OnCheckedChangeListener { buttonView, isChecked -> @@ -145,14 +172,25 @@ class LoginActivity : BaseActivityOaToolbarViewBinding() { binding.tvServiceAgreement.setOnClickListener { val intent = Intent(this, ServiceAgreementActivity::class.java) + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP) startActivity(intent) } binding.tvPrivacy.setOnClickListener { val intent = Intent(this, PrivacyAgreementActivity::class.java) + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP) startActivity(intent) } - + //vpn登录 + binding.tvVpn.setOnClickListener { + goToVpnLogin() + } + //vpn注销 + binding.tvVpnLogout.setOnClickListener { + // 注销VPN登录. + SFUemSDK.getInstance().logout() + showToast(R.string.str_vpn_logout) + } } @SuppressLint("SetTextI18n") @@ -174,8 +212,8 @@ class LoginActivity : BaseActivityOaToolbarViewBinding() { if (isApkInDebug(context)) { // 管理员 -// binding.etUsername.setText("ZHPS_Admin") -// binding.etPassword.setText("Rehome.zhps@996") + binding.etUsername.setText("ZHPS_Admin") + binding.etPassword.setText("Rehome.zhps@996") //公司内网215管理员 @@ -256,6 +294,7 @@ class LoginActivity : BaseActivityOaToolbarViewBinding() { this, WjmmGetVerificationCodeActivity::class.java ) + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP) startActivity(intent) }) @@ -560,6 +599,75 @@ class LoginActivity : BaseActivityOaToolbarViewBinding() { System.exit(0) } + private fun checkInput(): Boolean{ + if (TextUtils.isEmpty(binding.etUsername.text.toString())) { + showToast("用户名不能为空") + return false + } + if (TextUtils.isEmpty(binding.etPassword.text.toString())) { + showToast("密码不能为空") + return false + } + return true; + } + + private fun goToVpnLogin() { + val usernameVpnTemp = SPUtils.get(context, Contans.USER_NAME_VPN, "") as String + val pwdVpnTemp = SPUtils.get(context, Contans.PASS_WORD_VPN, "") as String + + + if ((!TextUtils.isEmpty(usernameVpnTemp)) && (!TextUtils.isEmpty(pwdVpnTemp))) { + usernameVpn = RSAUtils.decryptBASE64StrLocal(usernameVpnTemp) + pwdVpn = RSAUtils.decryptBASE64StrLocal(pwdVpnTemp) + } + + //val intent = Intent(context, VpnLoginActivity::class.java) + + val intent = Intent(context, VpnAuthActivity::class.java) + + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP) + if ((!TextUtils.isEmpty(usernameVpn)) && (!TextUtils.isEmpty(pwdVpn))) { + intent.putExtra("usernameVpn",usernameVpn) + intent.putExtra("pwdVpn",pwdVpn) + } + launcherResultVpn.launch(intent) + } + //创建一个ActivityResultLauncher + private fun createActivityResultLauncherVpn(): ActivityResultLauncher { + //kotlin写法 + return registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { + val data = it.data + val resultCode = it.resultCode + if (resultCode == RESULT_OK) { + if (data != null) { + usernameVpn = data.getStringExtra("usernameVpn").toString() + pwdVpn = data.getStringExtra("pwdVpn").toString() + showLog(usernameVpn) + showLog(pwdVpn) + SPUtils.put( + context, + Contans.USER_NAME_VPN, + RSAUtils.encryptBASE64StrLocal(usernameVpn) + ) + SPUtils.put( + context, + Contans.PASS_WORD_VPN, + RSAUtils.encryptBASE64StrLocal(pwdVpn) + ) + if(checkInput()){ + //登录 + appLogin() + +// val intent = Intent(context, AuthSuccessActivity::class.java) +// intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP) +// startActivity(intent) +// finish() + + } + } + } + } + } } diff --git a/app/src/main/java/com/rehome/zhdcoa/ui/activity/PrimaryAuthActivity.java b/app/src/main/java/com/rehome/zhdcoa/ui/activity/PrimaryAuthActivity.java new file mode 100644 index 0000000..a9df515 --- /dev/null +++ b/app/src/main/java/com/rehome/zhdcoa/ui/activity/PrimaryAuthActivity.java @@ -0,0 +1,490 @@ +package com.rehome.zhdcoa.ui.activity; + + +import android.app.AlertDialog; +import android.app.Dialog; +import android.app.ProgressDialog; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.SharedPreferences; +import android.os.Bundle; +import android.text.TextUtils; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.Button; +import android.widget.EditText; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.appcompat.app.AppCompatActivity; + +import com.sangfor.sdk.SFUemSDK; +import com.sangfor.sdk.base.SFAuthResultListener; +import com.sangfor.sdk.base.SFAuthType; +import com.sangfor.sdk.base.SFBaseMessage; +import com.sangfor.sdk.base.SFChangePswMessage; +import com.sangfor.sdk.base.SFConstants; +import com.sangfor.sdk.base.SFSetSpaConfigListener; +import com.sangfor.sdk.base.SFSmsMessage; +import com.sangfor.sdk.utils.SFLogN; +import com.rehome.zhdcoa.R; +import com.rehome.zhdcoa.vpn.Constants; +//import com.rehome.zhdcoa.activity.AuthSuccessActivity; +import com.rehome.zhdcoa.vpn.SFDialogHelper; +import java.util.HashMap; +import java.util.Map; + + +public class PrimaryAuthActivity extends AppCompatActivity implements SFAuthResultListener { + private static final String TAG = "app"; + + private String mServerAddress = "https://vpn.zhp.geg.com.cn:55420"; //服务器地址 + private String mUserName = "rehome5"; //用户名 + private String mUserPassword = "452131wW,./"; //密码 + + private EditText mServerAddressEditText = null; + private EditText mUserNameEditView = null; + private EditText mUserPasswordEditView = null; + private Button mLoginButton = null; + private AlertDialog mAuthDialog = null; + + private ProgressDialog mProgressDialog = null; // 进度条对话框 + + @Override + protected void onCreate(Bundle savedInstanceState) { + Log.i(TAG, "onCreate..."); + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_primary_auth); + //加载UI + initView(); + + //SPA初始化,设置安全码 + String config = "{\"loginAddress\":\"https://vpn.zhp.geg.com.cn:55420\", \"spaSecret\":\"7FFQ-rWE5-V6xX\"}"; + SFUemSDK.setSpaConfig(config, new SFSetSpaConfigListener() { + @Override + public void onSetSpaConfig(String result, SFBaseMessage error) { + SFLogN.info(TAG, "spa result:"+ result + ", error:" + error); + Log.i(TAG, "spa result:"+ result + ", error:" + error); + if (error.mErrCode != 0) { + Toast.makeText(PrimaryAuthActivity.this, + "SPA设置失败"+ ", Error Message:" + error.toString(), + Toast.LENGTH_SHORT) + .show(); + } else { + Toast.makeText(PrimaryAuthActivity.this, "SPA设置成功, result:" + result, + Toast.LENGTH_SHORT) + .show(); + } + } + }); + + + //加载上次登录信息 + setLoginInfo(); + /** + * 设置认证回调,认证结果在SFAuthResultListener的onAuthSuccess、onAuthFailed、onAuthProgress中返回 + * 如果不设置,将接收不到认证结果回调 + */ + SFUemSDK.getInstance().setAuthResultListener(this); + + //免密认证 + startAutoTicket(); + } + + private void startAutoTicket() { + /** + * 这里是自动免密认证接口,返回true表示认证成功,此时用户就可以进行资源访问了, + * 如果返回false,表示当前不满足自动免密条件,需要用户主动调用用户名密码认证接口 + */ + if (SFUemSDK.getInstance().startAutoTicket()){ + showToast("免密成功"); +// startActivity(new Intent(PrimaryAuthActivity.this, AuthSuccessActivity.class)); +// finish(); + } + } + + @Override + protected void onResume() { + Log.i(TAG,"onResume... "); + super.onResume(); + } + + @Override + protected void onNewIntent(Intent intent) { + Log.i(TAG, "onNewIntent..."); + super.onNewIntent(intent); + } + + @Override + protected void onPause() { + super.onPause(); + + //只有走回收流程的时候的那种onPause,isFinishing才为true + if (isFinishing()) { + //取消认证回调 + SFLogN.info(TAG,"SFUemSDK setAuthResultListener null"); + /** + * 注意: 清除回调建议放到onPause()方法而不是onDestroy()中, + * 避免出现onDestroy()在onCreate()之后执行,onCreate注册的认证回调被onDestory清空的问题 + */ + SFUemSDK.getInstance().setAuthResultListener(null); + } + } + + // 初始化界面元素 + private void initView() { + mServerAddressEditText = findViewById(R.id.vpn_addr_editView); + mUserNameEditView = findViewById(R.id.svpn_username_editView); + mUserPasswordEditView = findViewById(R.id.svpn_userPassword_editView); + //登录按钮 + mLoginButton = findViewById(R.id.svpn_login_button); + mLoginButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + mServerAddress = mServerAddressEditText.getText().toString(); + mUserName = mUserNameEditView.getText().toString(); + mUserPassword = mUserPasswordEditView.getText().toString(); + //开始主认证 + startPrimaryAuth(); + } + }); + } + + //开始用户名密码认证 + private void startPrimaryAuth() { + + if (TextUtils.isEmpty(mServerAddress)) { + showToast("VPN服务器地址不能为空"); + return; + } + + if (TextUtils.isEmpty(mUserName)) { + showToast("用户名不能为空"); + return; + } + + showWaitingDialog(false); + + /** + * 开始用户名密码认证,认证结果会在认证回调onAuthSuccess,onAuthFailed,onAuthProgress中返回 + */ + SFUemSDK.getInstance().startPasswordAuth(mServerAddress, mUserName, mUserPassword); + + } + + /** + * 认证成功 + * + * @param message 认证成功信息 + */ + @Override + public void onAuthSuccess(final SFBaseMessage message) { + SFLogN.info(TAG, "auth success"); + + //关闭进度框 + dismissWaitingDialog(); + + saveLoginInfo(); //保存登录信息 + + //认证完成,跳转认证成功界面,可以开始访问资源 + runOnUiThread(new Runnable() { + @Override + public void run() { + showToast(getString(R.string.auth_success)); +// startActivity(new Intent(PrimaryAuthActivity.this, AuthSuccessActivity.class)); +// finish(); + } + }); + } + + /** + * 认证失败 + * + * @param message 认证失败信息 + */ + @Override + public void onAuthFailed(final SFBaseMessage message) { + SFLogN.error2(TAG, "auth failed", "errMsg: " + message.mErrStr); + + //关闭进度框 + dismissWaitingDialog(); + + //认证失败 + runOnUiThread(new Runnable() { + @Override + public void run() { + showErrorMessage(message.mErrStr); + } + }); + } + + /** + * 主认证成功,但需要辅助认证(下一步认证) + * + * @param nextAuthType 下一步认证类型 + * @param message 下一步认证信息 + */ + @Override + public void onAuthProgress(SFAuthType nextAuthType, SFBaseMessage message) { + SFLogN.info(TAG, "need next auth, authType: " + nextAuthType.name()); + dismissWaitingDialog(); + + /** + * 1. 二次认证回调onAuthProgress, 不仅仅是管理员配置了二次认证才会回调; + * 如果管理员启用了"首次登录强制修改密码", 用户首次认证时会回调onAuthProgress; + * 如果管理员设置了密码有效期, 在密码过期后, 重新登录的时候也会回调onAuthProgress. + * + * 2. 二次认证回调中建议对修改密码进行处理, 对其他不做支持的二次认证需要做一个兜底处理: + * 针对不支持的认证方式做一个提示, 避免出现管理员配置了二次认证后, + * 用户侧收到了二次认证回调, 认证流程未完成但是无任何感知. + */ + if (nextAuthType == SFAuthType.AUTH_TYPE_RENEW_PASSWORD) { + //显示下一步认证UI界面 + showAuthDialog(nextAuthType, message); + return; + } + + showErrorMessage("暂不支持此种认证类型(" + nextAuthType.toString() + ")"); + } + + + //显示“请稍候...”提示框 + public void showWaitingDialog(final boolean isCancelable) { + runOnUiThread(new Runnable() { + @Override + public void run() { + if (mProgressDialog == null || !mProgressDialog.isShowing()) { + mProgressDialog = new ProgressDialog(PrimaryAuthActivity.this); + mProgressDialog.setProgressStyle(ProgressDialog.STYLE_SPINNER); + mProgressDialog.setTitle(""); + mProgressDialog.setMessage(PrimaryAuthActivity.this.getString(R.string.waiting)); + mProgressDialog.setCancelable(isCancelable); + mProgressDialog.show(); + } + } + }); + } + + //取消“请稍候...”提示框 + public void dismissWaitingDialog() { + runOnUiThread(new Runnable() { + @Override + public void run() { + if (mProgressDialog != null && mProgressDialog.isShowing()) { + mProgressDialog.dismiss(); + mProgressDialog = null; + } + } + }); + } + + //显示错误提示对话框 + public void showErrorMessage(final String errMsg) { + runOnUiThread(new Runnable() { + @Override + public void run() { + Dialog messageDialog = new AlertDialog.Builder(PrimaryAuthActivity.this) + .setTitle(PrimaryAuthActivity.this.getString(R.string.info)) + .setMessage(errMsg) + .setIcon(android.R.drawable.ic_dialog_alert) + .setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + dialog.dismiss(); + } + }) + .create(); + + messageDialog.show(); + } + }); + } + + //封装Toast + public void showToast(final String text) { + runOnUiThread(new Runnable() { + @Override + public void run() { + Toast.makeText(PrimaryAuthActivity.this, text, Toast.LENGTH_SHORT).show(); + } + }); + } + + + //恢复登录信息,重启应用后记录原来的登陆信息 + private void setLoginInfo() { + SharedPreferences sharedPreferences = getSharedPreferences(Constants.PREF_FILE_NAME, MODE_PRIVATE); + mServerAddress = sharedPreferences.getString(Constants.KEY_VPN_ADDRESS, mServerAddress); + mUserName = sharedPreferences.getString(Constants.KEY_USER_NAME, mUserName); + mUserPassword = sharedPreferences.getString(Constants.KEY_USER_PASSWORD, mUserPassword); + + mServerAddressEditText.setText(mServerAddress); + mUserNameEditView.setText(mUserName); + mUserPasswordEditView.setText(mUserPassword); + } + + //更新保存的登录信息 + private void saveLoginInfo() { + SharedPreferences sharedPreferences = getSharedPreferences(Constants.PREF_FILE_NAME, MODE_PRIVATE); + SharedPreferences.Editor editor = sharedPreferences.edit(); + editor.putString(Constants.KEY_VPN_ADDRESS, mServerAddress); + //保存用户名和密码,真实场景请加密存储 + editor.putString(Constants.KEY_USER_NAME, mUserName); + editor.putString(Constants.KEY_USER_PASSWORD, mUserPassword); + editor.apply(); + } + + /** + * 显示修改密码UI + */ + private void showAuthDialog(final SFAuthType authType, final SFBaseMessage message) { + runOnUiThread(new Runnable() { + @Override + public void run() { + closeDialog(); + //获取认证类型对应的标题 + String dialogTitle = SFDialogHelper.getDialogTitle(authType); + if (TextUtils.isEmpty(dialogTitle)) { + showErrorMessage("暂不支持此种认证类型(" + authType.toString() + ")"); + return; + } + //获取认证类型对应的layout布局 + int viewLayoutId = SFDialogHelper.getAuthDialogViewId(authType); + handlerSecondAuth(dialogTitle, viewLayoutId, authType, message); + } + }); + } + + /** + * 关闭对话框 + */ + private void closeDialog() { + if (mAuthDialog != null && mAuthDialog.isShowing()) { + mAuthDialog.dismiss(); + mAuthDialog = null; + } + } + + /** + * 处理二次验证 + * @param dialogTitle + * @param viewLayoutId + * @param authType + * @param message + */ + private void handlerSecondAuth(String dialogTitle, int viewLayoutId, SFAuthType authType, SFBaseMessage message) { + + //创建认证类型对应的对话框显示视图 + final View dialogView = createDialogView(authType, viewLayoutId, message); + mAuthDialog = new AlertDialog.Builder(PrimaryAuthActivity.this) + .setTitle(dialogTitle) + .setView(dialogView) + .setCancelable(false) + .setPositiveButton(R.string.str_commit, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + closeDialog(); + //构建辅助认证类型所需参数 + Map authParams = new HashMap(); + String errorMsg = buildAuthParams(authType, dialogView, authParams); + //检查参数构建是否成功 + if (TextUtils.isEmpty(errorMsg)) { + showWaitingDialog(false); + //开始辅助认证 + SFUemSDK.getInstance().doSecondaryAuth(authType, authParams); + } else { + //参数构建失败,进行提示 + showErrorMessage(errorMsg); + } + return; + } + }) + .setNegativeButton(R.string.str_cancel, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + //放弃继续认证 + closeDialog(); + return; + } + }) + .create(); + //显示认证对话框 + mAuthDialog.show(); + } + + /** + * 创建认证对话框中间显示的视图 + * + * @param sfAuthType 认证类型 + * @param layoutId 要加载的视图的布局ID + * @param message 认证附加信息 + * @return 认证对话框视图 + */ + private View createDialogView(SFAuthType sfAuthType, int layoutId, final SFBaseMessage message) { + LayoutInflater inflater = getLayoutInflater(); + View dialogView = inflater.inflate(layoutId, null); + switch (sfAuthType) { + case AUTH_TYPE_RENEW_PASSWORD: { + TextView tvPolicy = (TextView) dialogView.findViewById(R.id.tv_policy); + String policy = ""; + //获取密码策略 + if (message instanceof SFChangePswMessage) { + policy = ((SFChangePswMessage) message).policyMsg; + } + if (TextUtils.isEmpty(policy)) { + tvPolicy.setText(R.string.str_no_policy); + } else { + tvPolicy.setText(getString(R.string.str_policy_hint) + "\n" + policy); + } + break; + } + default: + break; + } + + return dialogView; + } + + /** + * 构建辅助认证所需参数 + * + * @param sfAuthType 认证类型 + * @param dialogView 认证视图 + * @param authParams 保存认证参数 + * @return 参数错误信息 + */ + private String buildAuthParams(SFAuthType sfAuthType, View dialogView, Map authParams) { + String errorMsg = ""; + switch (sfAuthType) { + case AUTH_TYPE_RENEW_PASSWORD: { + EditText etNewPwd = dialogView.findViewById(R.id.et_newpwd); + EditText etOldPwd = dialogView.findViewById(R.id.et_oldpwd); + EditText etReNewPwd = dialogView.findViewById(R.id.et_renewpwd); + String oldPwd = etOldPwd.getText().toString(); + String newPwd = etNewPwd.getText().toString(); + String reNewPwd = etReNewPwd.getText().toString(); + + if (TextUtils.isEmpty(newPwd) || TextUtils.isEmpty(reNewPwd)) { + errorMsg = "新密码不能为空"; + break; + } + + if (!newPwd.equals(reNewPwd)) { + errorMsg = "新密码与确认密码不一致"; + break; + } + + authParams.put(SFConstants.AUTH_KEY_RENEW_OLD_PASSWORD, oldPwd); + authParams.put(SFConstants.AUTH_KEY_REWNEW_NEW_PASSWORD, newPwd); + break; + } + default: + errorMsg = "暂不支持的认证类型"; + break; + } + + return errorMsg; + } + +} diff --git a/app/src/main/java/com/rehome/zhdcoa/ui/activity/PrimarySmsLoginActivity.java b/app/src/main/java/com/rehome/zhdcoa/ui/activity/PrimarySmsLoginActivity.java new file mode 100644 index 0000000..6cc0713 --- /dev/null +++ b/app/src/main/java/com/rehome/zhdcoa/ui/activity/PrimarySmsLoginActivity.java @@ -0,0 +1,719 @@ +package com.rehome.zhdcoa.ui.activity; + + +import android.app.Activity; +import android.app.ActivityManager; +import android.app.AlertDialog; +import android.app.Dialog; +import android.app.ProgressDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.graphics.Color; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.os.CountDownTimer; +import android.os.Process; +import android.text.TextUtils; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.Button; +import android.widget.EditText; +import android.widget.ImageView; +import android.widget.RadioGroup; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.appcompat.app.AppCompatActivity; + +import com.sangfor.sdk.SFUemSDK; +import com.sangfor.sdk.base.SFAuthResultListener; +import com.sangfor.sdk.base.SFAuthType; +import com.sangfor.sdk.base.SFBaseMessage; +import com.sangfor.sdk.base.SFChangePswMessage; +import com.sangfor.sdk.base.SFConstants; +import com.sangfor.sdk.base.SFRegetRandCodeListener; +import com.sangfor.sdk.base.SFRegetSmsListener; +import com.sangfor.sdk.base.SFSetSpaConfigListener; +import com.sangfor.sdk.base.SFSmsMessage; +import com.sangfor.sdk.utils.SFLogN; + +import com.rehome.zhdcoa.R; +import com.rehome.zhdcoa.vpn.CertUtils; +import com.rehome.zhdcoa.vpn.Constants; +import com.rehome.zhdcoa.vpn.PermissionUtil; +import com.rehome.zhdcoa.vpn.SFDialogHelper; + +import java.io.ByteArrayInputStream; +import java.security.MessageDigest; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +public class PrimarySmsLoginActivity extends AppCompatActivity implements SFAuthResultListener, SFRegetSmsListener, + SFRegetRandCodeListener { + private static final String TAG = "app"; + private static final int PERMISSON_REQUEST_CODE = 32; //动态权限请求码 + private static final int CERTFILE_REQUEST_CODE = 33; //当证书认证是主认证时获取证书路径请求码 + private static final int DIALOG_CERTFILE_REQUEST_CODE = 34; //当证书认证是辅助认证时获取证书路径请求码 + private static final int DEFAULT_SMS_COUN_TDOWN_TIME = 30; //短信验证码默认倒计时时间, 以秒为单位 + + private String mVpnAddress = "https://vpn.zhp.geg.com.cn:55420"; + +// enum SERVER_MODE { +// VPN, +// SDP +// } + + //private SERVER_MODE mServerMode = SERVER_MODE.SDP; + + //private String mSdpAddress = "https://10.242.4.235:443"; + private String mPhoneNumber = "15307858521"; + + //View + private AlertDialog mAuthDialog = null; + private EditText mVpnAddressEditText = null; + private EditText mPhoneNumberEditView = null; + private Button mLoginButton = null; + + private View mRandCodeDialogView = null; //图形校验码对话框视图 + private View mSmsCodeDialogView = null; //短信验证码对话框视图 + private EditText mCertPathDialogEditView = null;//证书路径输入框 + private ProgressDialog mProgressDialog = null; // 对话框对象 + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_vpn_auth); + //加载UI + initView(); + //SPA初始化,设置安全码 + String config = "{\"loginAddress\":\"https://vpn.zhp.geg.com.cn:55420\", \"spaSecret\":\"7FFQ-rWE5-V6xX\"}"; + SFUemSDK.setSpaConfig(config, new SFSetSpaConfigListener() { + @Override + public void onSetSpaConfig(String result, SFBaseMessage error) { + SFLogN.info(TAG, "spa result:"+ result + ", error:" + error); + Log.i(TAG, "spa result:"+ result + ", error:" + error); + if (error.mErrCode != 0) { + Toast.makeText(PrimarySmsLoginActivity.this, + "SPA设置失败"+ ", Error Message:" + error.toString(), + Toast.LENGTH_SHORT) + .show(); + } else { + Toast.makeText(PrimarySmsLoginActivity.this, "SPA设置成功, result:" + result, + Toast.LENGTH_SHORT) + .show(); + } + } + }); + +// SFUemSDK.setSpaConfig(config, new SFSetSpaConfigListener() { +// @Override +// public void onSetSpaConfig(String result, SFBaseMessage error) { +// Log.i(TAG, "spa result:"+ result + ", error:" + error); +// if (error.mErrCode != 0) { +// Toast.makeText(PrimarySmsLoginActivity.this, +// "SPA设置失败"+ ", Error Message:" + error.toString(), +// Toast.LENGTH_SHORT) +// .show(); +// } else { +// Toast.makeText(PrimarySmsLoginActivity.this, "SPA设置成功, result:" + result, +// Toast.LENGTH_SHORT) +// .show(); +// +// } +// } +// }); + + + //加载上次登录信息 + setLoginInfo(); + /** + * 设置认证回调,认证结果在SFAuthResultListener的onAuthSuccess、onAuthFailed、onAuthProgress中返回 + * 如果不设置,将接收不到认证结果 + */ + SFUemSDK.getInstance().setAuthResultListener(this); + + if (SFUemSDK.getInstance().startAutoTicket()){ + showToast("免密成功"); + //startActivity(new Intent(PrimarySmsLoginActivity.this, AuthSuccessActivity.class)); + } + + } + + @Override + protected void onResume() { + super.onResume(); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + //取消认证回调 + SFLogN.info(TAG,"SFUemSDK setAuthResultListener "); + SFUemSDK.getInstance().setAuthResultListener(null); + } + + /** + * 初始化界面元素 + */ + private void initView() { + mVpnAddressEditText = findViewById(R.id.vpn_addr_editView); + mPhoneNumberEditView = findViewById(R.id.svpn_username_editView); + //登录按钮 + mLoginButton = findViewById(R.id.svpn_login_button); + mLoginButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + //开始主认证,用户名密码或者证书认证,在认证回调中返回认证结果 + startPrimaryAuth(); + } + }); + } + + /** + * 开始主认证,用户名密码或者证书认证 + */ + private void startPrimaryAuth() { + mVpnAddress = mVpnAddressEditText.getText().toString(); + mPhoneNumber = mPhoneNumberEditView.getText().toString(); + + if (TextUtils.isEmpty(mVpnAddress)) { + showToast("VPN服务器地址不能为空"); + return; + } + + if (TextUtils.isEmpty(mPhoneNumber)) { + showToast("手机号码不能为空"); + return; + } + + showWaitingDialog(false); + SFUemSDK.getInstance().startPrimarySmsAuth(mVpnAddress, mPhoneNumber); + } + + /** + * 认证成功 + * + * @param message 认证成功信息 + */ + @Override + public void onAuthSuccess(final SFBaseMessage message) { + SFLogN.info(TAG, "auth success"); + dismissWaitingDialog(); + + saveLoginInfo(); //保存登录信息 + + //认证完成,跳转认证成功界面,可以开始访问资源 + runOnUiThread(new Runnable() { + @Override + public void run() { + showToast(getString(R.string.auth_success)); + //startActivity(new Intent(PrimarySmsLoginActivity.this, AuthSuccessActivity.class)); + } + }); + } + + /** + * 认证失败 + * + * @param message 认证失败信息 + */ + @Override + public void onAuthFailed(final SFBaseMessage message) { + SFLogN.error2(TAG, "auth failed", "errMsg: " + message.mErrStr); + dismissWaitingDialog(); + + //认证失败 + runOnUiThread(new Runnable() { + @Override + public void run() { + showErrorMessage(message.mErrStr); + } + }); + } + + /** + * 主认证成功,但需要辅助认证(下一步认证) + * + * @param nextAuthType 下一步认证类型 + * @param message 下一步认证信息 + */ + @Override + public void onAuthProgress(SFAuthType nextAuthType, SFBaseMessage message) { + SFLogN.info(TAG, "need next auth, authType: " + nextAuthType.name()); + dismissWaitingDialog(); + //显示下一步认证UI界面 + showAuthDialog(nextAuthType, message); + } + + /** + * 显示下一步认证UI界面 + */ + public void showAuthDialog(final SFAuthType authType, final SFBaseMessage message) { + runOnUiThread(new Runnable() { + @Override + public void run() { + closeDialog(); + //获取认证类型对应的标题 + String dialogTitle = SFDialogHelper.getDialogTitle(authType); + if (TextUtils.isEmpty(dialogTitle)) { + showErrorMessage("暂不支持此种认证类型(" + authType.toString() + ")"); + return; + } + //获取认证类型对应的layout布局 + int viewLayoutId = SFDialogHelper.getAuthDialogViewId(authType); + //创建认证类型对应的对话框显示视图 + final View dialogView = createDialogView(authType, viewLayoutId, message); + mAuthDialog = new AlertDialog.Builder(PrimarySmsLoginActivity.this) + .setTitle(dialogTitle) + .setView(dialogView) + .setCancelable(false) + .setPositiveButton(R.string.str_commit, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + closeDialog(); + //构建辅助认证类型所需参数 + Map authParams = new HashMap(); + String errorMsg = buildAuthParams(authType, dialogView, authParams); + //检查参数构建是否成功 + if (TextUtils.isEmpty(errorMsg)) { + showWaitingDialog(false); + //开始辅助认证 + SFUemSDK.getInstance().doSecondaryAuth(authType, authParams); + } else { + //参数构建失败,进行提示 + showErrorMessage(errorMsg); + } + return; + } + }) + .setNegativeButton(R.string.str_cancel, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + //放弃继续认证 + closeDialog(); + return; + } + }) + .create(); + //显示认证对话框 + mAuthDialog.show(); + } + }); + } + + /** + * 创建认证对话框中间显示的视图 + * + * @param sfAuthType 认证类型 + * @param layoutId 要加载的视图的布局ID + * @param message 认证附加信息 + * @return 认证对话框视图 + */ + private View createDialogView(SFAuthType sfAuthType, int layoutId, final SFBaseMessage message) { + LayoutInflater inflater = getLayoutInflater(); + View dialogView = inflater.inflate(layoutId, null); + switch (sfAuthType) { + case AUTH_TYPE_SMS: + case AUTH_TYPE_PRIMARY_SMS: { + mSmsCodeDialogView = dialogView; + TextView tvTel = dialogView.findViewById(R.id.tv_tel); + Button btnGetVerficationCode = dialogView.findViewById(R.id.bt_getVerficationCode); + btnGetVerficationCode.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + v.setEnabled(false); + //调用重新获取短信接口 + SFUemSDK.getInstance().getSFAuth().regetSmsCode(PrimarySmsLoginActivity.this); + } + }); + + SFSmsMessage smsMessage = ((SFSmsMessage) message); + //获取手机号码 + String smsPhoneNum = smsMessage.getPhoneNum(); + if (TextUtils.isEmpty(smsPhoneNum)) { + tvTel.setText(R.string.get_phone_number_failed); + } else { + tvTel.setText(getString(R.string.phone_number) + smsPhoneNum); + } + //上次发送的短信验证码是否在有效期 + if (smsMessage.isStillValid()) { + showToast("上次发送的短信验证码仍在有效期"); + } + //开启短信码倒计时 + smsCountDownTimer(btnGetVerficationCode, smsMessage.getCountDown()); + break; + } + case AUTH_TYPE_RENEW_PASSWORD: { + TextView tvPolicy = (TextView) dialogView.findViewById(R.id.tv_policy); + String policy = ""; + //获取密码策略 + if (message instanceof SFChangePswMessage) { + policy = ((SFChangePswMessage) message).policyMsg; + } + if (TextUtils.isEmpty(policy)) { + tvPolicy.setText(R.string.str_no_policy); + } else { + tvPolicy.setText(getString(R.string.str_policy_hint) + "\n" + policy); + } + break; + } + case AUTH_TYPE_RAND: { + mRandCodeDialogView = dialogView;// 保存一份图形校验码对话框视图 + final ImageView imgError = dialogView.findViewById(R.id.randcode_imgError); + final ImageView imgRandCode = dialogView.findViewById(R.id.iv_graphCode); + + // 按图片刷新校验码 + View.OnClickListener refreshListener = new View.OnClickListener() { + public void onClick(View v) { + long lastRefresh = (Long) imgRandCode.getTag(); + //检查更新间隔,暂定间隔3秒 + if (System.currentTimeMillis() - lastRefresh >= 3 * 1000) { + // 启动刷新任务 + SFUemSDK.getInstance().getSFAuth().regetRandCode(PrimarySmsLoginActivity.this); + imgRandCode.setTag(System.currentTimeMillis()); + } + } + }; + + imgError.setOnClickListener(refreshListener); + imgRandCode.setOnClickListener(refreshListener); + + SFUemSDK.getInstance().getSFAuth().regetRandCode(PrimarySmsLoginActivity.this); //首次自动触发获取校验码 + imgRandCode.setTag(System.currentTimeMillis());// 记录上次更新时间到imgChecksum控件的tag里,初始化为0,即没有上次更新 + break; + } + default: + break; + } + + return dialogView; + } + + /** + * 短信验证码倒计时器 + * + * @param button 显示计时的按钮控件 + */ + private void smsCountDownTimer(final Button button, final int countDown) { + int smsRefreshTime = countDown < 0 ? DEFAULT_SMS_COUN_TDOWN_TIME : countDown; + //开启短信验证码倒计时,第一个参数为倒计时时间(毫秒),第二个为时间间隔 + new CountDownTimer(smsRefreshTime * 1000, 1000) { + @Override + public void onTick(long millisUntilFinished) { + button.setText(millisUntilFinished / 1000 + getString(R.string.after_time_resend)); + button.setTextColor(Color.parseColor("#708090")); + button.setEnabled(false); + } + + @Override + public void onFinish() { + button.setText(R.string.str_resend); + button.setTextColor(Color.parseColor("#000000")); + button.setEnabled(true); + } + }.start(); + } + + /** + * 构建辅助认证所需参数 + * + * @param sfAuthType 认证类型 + * @param dialogView 认证视图 + * @param authParams 保存认证参数 + * @return 参数错误信息 + */ + private String buildAuthParams(SFAuthType sfAuthType, View dialogView, Map authParams) { + String errorMsg = ""; + switch (sfAuthType) { + case AUTH_TYPE_SMS: { + EditText etSmsCode = dialogView.findViewById(R.id.et_verficationCode); + String smsCode = etSmsCode.getText().toString(); + + if (TextUtils.isEmpty(smsCode)) { + errorMsg = "短信验证码不能为空"; + break; + } + + authParams.put(SFConstants.AUTH_KEY_SMS, smsCode); + break; + } + case AUTH_TYPE_PRIMARY_SMS: { + EditText etSmsCode = dialogView.findViewById(R.id.et_verficationCode); + String smsCode = etSmsCode.getText().toString(); + + if (TextUtils.isEmpty(smsCode)) { + errorMsg = "短信验证码不能为空"; + break; + } + + authParams.put(SFConstants.AUTH_KEY_PRIMARYSMS_CODE, smsCode); + break; + } + case AUTH_TYPE_RADIUS: { + EditText etRadiusAnswer = dialogView.findViewById(R.id.et_authAnswer); + String radiusAnswer = etRadiusAnswer.getText().toString(); + + if (TextUtils.isEmpty(radiusAnswer)) { + errorMsg = "认证答案不能为空"; + break; + } + + authParams.put(SFConstants.AUTH_KEY_RADIUS_CODE, radiusAnswer); + break; + } + case AUTH_TYPE_TOKEN: { + EditText etDynamicToken = dialogView.findViewById(R.id.et_dynamicToken); + String dynamicToken = etDynamicToken.getText().toString(); + + if (TextUtils.isEmpty(dynamicToken)) { + errorMsg = "动态口令不能为空"; + break; + } + + authParams.put(SFConstants.AUTH_KEY_TOKEN, dynamicToken); + break; + } + + case AUTH_TYPE_RENEW_PASSWORD: { + EditText etNewPwd = dialogView.findViewById(R.id.et_newpwd); + EditText etOldPwd = dialogView.findViewById(R.id.et_oldpwd); + EditText etReNewPwd = dialogView.findViewById(R.id.et_renewpwd); + String oldPwd = etOldPwd.getText().toString(); + String newPwd = etNewPwd.getText().toString(); + String reNewPwd = etReNewPwd.getText().toString(); + + if (TextUtils.isEmpty(newPwd) || TextUtils.isEmpty(reNewPwd)) { + errorMsg = "新密码不能为空"; + break; + } + + if (!newPwd.equals(reNewPwd)) { + errorMsg = "新密码与确认密码不一致"; + break; + } + + authParams.put(SFConstants.AUTH_KEY_RENEW_OLD_PASSWORD, oldPwd); + authParams.put(SFConstants.AUTH_KEY_REWNEW_NEW_PASSWORD, newPwd); + break; + } + default: + errorMsg = "暂不支持的认证类型"; + break; + } + + return errorMsg; + } + + /** + * 关闭对话框 + */ + private void closeDialog() { +// if (mAuthDialog != null && mAuthDialog.isShowing()) { +// mAuthDialog.dismiss(); +// mAuthDialog = null; +// } + } + + /** + * @param success true: 成功, false: 失败 + * @param bytes 图片二进制数据,success为true是有效 + * @param byteLength 图片二进制数据长度,success为true是有效 + * @brief SFRegetRandCodeListener获取图形校验码回调 + */ + @Override + public void onRegetRandCode(boolean success, byte[] bytes, int byteLength) { + Drawable randCode = null; + if (success) { + randCode = Drawable.createFromStream(new ByteArrayInputStream(bytes), "rand_code"); + } else { + SFLogN.error2(TAG, "get RandCode failed", "maybe network is wrong"); + } + + final Drawable drawable = randCode; + //显示图形校验码 + runOnUiThread(new Runnable() { + @Override + public void run() { + if (mRandCodeDialogView == null) { + showToast("图形校验码对话框视图未加载"); + return; + } + + final ImageView imgError = mRandCodeDialogView.findViewById(R.id.randcode_imgError); + final ImageView imgRandCode = mRandCodeDialogView.findViewById(R.id.iv_graphCode); + + imgError.setVisibility(drawable == null ? View.VISIBLE : View.GONE); + imgRandCode.setVisibility(drawable == null ? View.GONE : View.VISIBLE); + + // 显示图形校验码 + imgRandCode.setImageDrawable(drawable); + // 更新刷新时间 + imgRandCode.setTag(System.currentTimeMillis()); + } + }); + } + + /** + * 重新获取短信验证码回调 + * + * @param success 是否成功获取,true表示成功,false表示失败 + * @param message 包含短信相关信息 + */ + @Override + public void onRegetSmsCode(final boolean success, final SFSmsMessage message) { + SFLogN.info(TAG, "onRegetSmsCode result: " + success + ", msg: " + message.mErrStr); + + //更新短信验证码信息 + runOnUiThread(new Runnable() { + @Override + public void run() { + if (mSmsCodeDialogView == null) { + showToast("短信验证码对话框视图未加载"); + return; + } + + final Button btnGetVerficationCode = mSmsCodeDialogView.findViewById(R.id.bt_getVerficationCode); + if (success) { + //更新倒计时 + smsCountDownTimer(btnGetVerficationCode, message.getCountDown()); + showToast(getString(R.string.reget_sms_code_success)); + } else { + btnGetVerficationCode.setEnabled(true); + showToast(getString(R.string.reget_sms_code_failed) + message.mErrStr); + } + } + }); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + switch (requestCode) { + //登录界面证书选择 + case CERTFILE_REQUEST_CODE: { + //获取证书选择器结果 + String certPath = ""; + if (resultCode == Activity.RESULT_OK) { + certPath = CertUtils.fromUriGetRealPath(this, data.getData()).trim(); + } + break; + } + case DIALOG_CERTFILE_REQUEST_CODE: { + //当证书认证是辅助认证时获取证书选择器结果 + String certPathDialog = ""; + if (resultCode == Activity.RESULT_OK) { + certPathDialog = CertUtils.fromUriGetRealPath(this, data.getData()).trim(); + } + mCertPathDialogEditView.setText(certPathDialog); + break; + } + } + super.onActivityResult(requestCode, resultCode, data); + } + + //显示“请稍候...”提示框 + public void showWaitingDialog(final boolean isCancelable) { + runOnUiThread(new Runnable() { + @Override + public void run() { + if (mProgressDialog == null || !mProgressDialog.isShowing()) { + mProgressDialog = new ProgressDialog(PrimarySmsLoginActivity.this); + mProgressDialog.setProgressStyle(ProgressDialog.STYLE_SPINNER); + mProgressDialog.setTitle(""); + mProgressDialog.setMessage(PrimarySmsLoginActivity.this.getString(R.string.waiting)); + mProgressDialog.setCancelable(isCancelable); + mProgressDialog.show(); + } + } + }); + } + + //取消“请稍候...”提示框 + public void dismissWaitingDialog() { + runOnUiThread(new Runnable() { + @Override + public void run() { + if (mProgressDialog != null && mProgressDialog.isShowing()) { + mProgressDialog.dismiss(); + mProgressDialog = null; + } + } + }); + } + + //显示错误提示对话框 + public void showErrorMessage(final String errMsg) { + runOnUiThread(new Runnable() { + @Override + public void run() { + Dialog messageDialog = new AlertDialog.Builder(PrimarySmsLoginActivity.this) + .setTitle(PrimarySmsLoginActivity.this.getString(R.string.info)) + .setMessage(errMsg) + .setIcon(android.R.drawable.ic_dialog_alert) + .setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + dialog.dismiss(); + } + }) + .create(); + + messageDialog.show(); + } + }); + } + + //封装Toast + public void showToast(final String text) { + runOnUiThread(new Runnable() { + @Override + public void run() { + Toast.makeText(PrimarySmsLoginActivity.this, text, Toast.LENGTH_SHORT).show(); + } + }); + } + + /** + * 权限授权结果回调 + */ + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, + @NonNull int[] paramArrayOfInt) { + super.onRequestPermissionsResult(requestCode, permissions, paramArrayOfInt); + if (requestCode == PERMISSON_REQUEST_CODE) { + //处理权限申请结果 + // do nothing + } + } + + /** + * 设置登录信息 + */ + private void setLoginInfo() { + SharedPreferences sharedPreferences = getSharedPreferences(Constants.PREF_FILE_NAME, MODE_PRIVATE); + mVpnAddress = sharedPreferences.getString(Constants.KEY_VPN_ADDRESS, mVpnAddress); + mPhoneNumber = sharedPreferences.getString(Constants.KEY_PHONE_NUMBER, mPhoneNumber); + + mVpnAddressEditText.setText(mVpnAddress); + mPhoneNumberEditView.setText(mPhoneNumber); + } + + /** + * 恢复保存的登录信息 + */ + private void saveLoginInfo() { + SharedPreferences sharedPreferences = getSharedPreferences(Constants.PREF_FILE_NAME, MODE_PRIVATE); + SharedPreferences.Editor editor = sharedPreferences.edit(); + editor.putString(Constants.KEY_VPN_ADDRESS, mVpnAddress); + //保存手机号码,真实场景请加密存储 + editor.putString(Constants.KEY_PHONE_NUMBER, mPhoneNumber); + editor.apply(); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/rehome/zhdcoa/ui/activity/TrainInfoMoreActivity.kt b/app/src/main/java/com/rehome/zhdcoa/ui/activity/TrainInfoMoreActivity.kt index 4cbb10d..ee33167 100644 --- a/app/src/main/java/com/rehome/zhdcoa/ui/activity/TrainInfoMoreActivity.kt +++ b/app/src/main/java/com/rehome/zhdcoa/ui/activity/TrainInfoMoreActivity.kt @@ -57,7 +57,7 @@ class TrainInfoMoreActivity : BaseActivityOaToolbarViewBinding = mutableListOf() private val dialogDatas: MutableList = mutableListOf() - private lateinit var launcherResult: ActivityResultLauncher + private var launcherResult: ActivityResultLauncher? = null override fun getViewBinding() = ActivityTrainInfoMoreBinding.inflate(layoutInflater) @@ -166,7 +166,7 @@ class TrainInfoMoreActivity : BaseActivityOaToolbarViewBinding +// } + + @Override + public int getLayoutId() { + return R.layout.activity_vpn_auth; + } + + private void startAutoTicket() { + /** + * 这里是自动免密认证接口,返回true表示认证成功,此时用户就可以进行资源访问了, + * 如果返回false,表示当前不满足自动免密条件,需要用户主动调用用户名密码认证接口 + */ + if (SFUemSDK.getInstance().startAutoTicket()){ + showToast("vpn免密登录认证成功"); + //startActivity(new Intent(PrimaryAuthActivity.this, AuthSuccessActivity.class)); + + /** + * 免密场景, 启动L3VPN隧道,需要等待onTunnelStarted返回启动隧道成功才能开始访问资源 + */ + SFUemSDK.getInstance().getSFTunnel().startTunnel(); + Intent intent = new Intent(); + intent.putExtra("usernameVpn", mUserName); + intent.putExtra("pwdVpn", mUserPassword); + setResult(RESULT_OK, intent); + finish(); + } + } + + + @Override + protected void onResume() { + Log.i(TAG,"onResume... "); + super.onResume(); + } + + @Override + protected void onNewIntent(Intent intent) { + Log.i(TAG, "onNewIntent..."); + super.onNewIntent(intent); + } + + // 初始化界面元素 + @Override + public void initView() { + mServerAddressEditText = findViewById(R.id.vpn_addr_editView); + mUserNameEditView = findViewById(R.id.svpn_username_editView); + mUserPasswordEditView = findViewById(R.id.svpn_userPassword_editView); + //登录按钮 + mLoginButton = findViewById(R.id.svpn_login_button); + mLoginButton.setOnClickListener(this); + + initToolbar("VPN登录", "", new View.OnClickListener() { + @Override + public void onClick(View v) { + + } + }); + + //SPA初始化,设置安全码 + String config = "{\"loginAddress\":\"https://vpn.zhp.geg.com.cn:55420\", \"spaSecret\":\"7FFQ-rWE5-V6xX\"}"; + SFUemSDK.setSpaConfig(config, new SFSetSpaConfigListener() { + @Override + public void onSetSpaConfig(String result, SFBaseMessage error) { + SFLogN.info(TAG, "spa result:"+ result + ", error:" + error); + Log.i(TAG, "spa result:"+ result + ", error:" + error); + if (error.mErrCode != 0) { + Toast.makeText(VpnAuthActivity.this, + "SPA设置失败"+ ", Error Message:" + error.toString(), + Toast.LENGTH_SHORT) + .show(); + } else { + Toast.makeText(VpnAuthActivity.this, "SPA设置成功, result:" + result, + Toast.LENGTH_SHORT) + .show(); + } + } + }); + + //加载上次登录信息 + setLoginInfo(); + + mUserNameEditView.setText("rehome5"); + mUserPasswordEditView.setText("452131wW,./"); + + + /** + * 设置认证回调,认证结果在SFAuthResultListener的onAuthSuccess、onAuthFailed、onAuthProgress中返回 + * 如果不设置,将接收不到认证结果回调 + */ + SFUemSDK.getInstance().setAuthResultListener(this); + + /** + * 设置隧道状态监听回调,启动,停止隧道的结果以及隧道状态变化会在SFTunnelStatusListener的onTunnelStarted、 + * onTunnelStoped,onTunnelStatusChanged中回调通知 + */ + SFUemSDK.getInstance().getSFTunnel().registerTunnelStatusListener(this); + + //免密认证 + startAutoTicket(); + + } + + @Override + public void initData() { + + } + + @Override + protected void onPause() { + super.onPause(); + + //只有走回收流程的时候的那种onPause,isFinishing才为true + if (isFinishing()) { + //取消认证回调 + SFLogN.info(TAG,"SFUemSDK setAuthResultListener null"); + /** + * 注意: 清除回调建议放到onPause()方法而不是onDestroy()中, + * 避免出现onDestroy()在onCreate()之后执行,onCreate注册的认证回调被onDestory清空的问题 + */ + SFUemSDK.getInstance().setAuthResultListener(null); + SFUemSDK.getInstance().getSFTunnel().unRegisterTunnelStatusListener(this); + } + } + + @Override + public void onClick(View v) { + if(v.getId()==R.id.svpn_login_button){ + mServerAddress = mServerAddressEditText.getText().toString(); + mUserName = mUserNameEditView.getText().toString(); + mUserPassword = mUserPasswordEditView.getText().toString(); + + //开始主认证 + startPrimaryAuth(); + } +// switch (v.getId()) { +// case R.id.svpn_login_button: { +// mServerAddress = mServerAddressEditText.getText().toString(); +// mUserName = mUserNameEditView.getText().toString(); +// mUserPassword = mUserPasswordEditView.getText().toString(); +// +// //开始主认证 +// startPrimaryAuth(); +// break; +// } +// default: +// break; +// } + } + + //开始用户名密码认证 + private void startPrimaryAuth() { + + if (TextUtils.isEmpty(mServerAddress)) { + showToast("VPN服务器地址不能为空"); + return; + } + + if (TextUtils.isEmpty(mUserName)) { + showToast("vpn帐号必填"); + return; + } + + if (TextUtils.isEmpty(mUserName)) { + showToast("vpn密码必填"); + return; + } + + showWaitingDialog(false); + + /** + * 开始用户名密码认证,认证结果会在认证回调onAuthSuccess,onAuthFailed,onAuthProgress中返回 + */ + SFUemSDK.getInstance().startPasswordAuth(mServerAddress, mUserName, mUserPassword); + } + + /** + * 认证成功 + * + * @param message 认证成功信息 + */ + @Override + public void onAuthSuccess(final SFBaseMessage message) { + showLog("auth success"); + + //关闭进度框 + dismissWaitingDialog(); + + saveLoginInfo(); //保存登录信息 + + /** + * 启动L3VPN隧道,需要等待onTunnelStarted返回启动隧道成功才能开始访问资源 + */ + SFUemSDK.getInstance().getSFTunnel().startTunnel(); + + + showLog(String.valueOf(SFUemSDK.getInstance().getAuthStatus())); + + //认证完成,跳转认证成功界面,可以开始访问资源 + runOnUiThread(new Runnable() { + @Override + public void run() { + showToast(getString(R.string.auth_success)); + //startActivity(new Intent(PrimaryAuthActivity.this, AuthSuccessActivity.class)); + + Intent intent = new Intent(); + intent.putExtra("usernameVpn", mUserName); + intent.putExtra("pwdVpn", mUserPassword); + setResult(RESULT_OK, intent); + finish(); + } + }); + } + + /** + * 认证失败 + * + * @param message 认证失败信息 + */ + @Override + public void onAuthFailed(final SFBaseMessage message) { + SFLogN.error2(TAG, "auth failed", "errMsg: " + message.mErrStr); + + //关闭进度框 + dismissWaitingDialog(); + + //认证失败 + runOnUiThread(new Runnable() { + @Override + public void run() { + showErrorMessage(message.mErrStr); + } + }); + } + + /** + * 主认证成功,但需要辅助认证(下一步认证) + * + * @param nextAuthType 下一步认证类型 + * @param message 下一步认证信息 + */ + @Override + public void onAuthProgress(SFAuthType nextAuthType, SFBaseMessage message) { + SFLogN.info(TAG, "need next auth, authType: " + nextAuthType.name()); + dismissWaitingDialog(); + + /** + * 1. 二次认证回调onAuthProgress, 不仅仅是管理员配置了二次认证才会回调; + * 如果管理员启用了"首次登录强制修改密码", 用户首次认证时会回调onAuthProgress; + * 如果管理员设置了密码有效期, 在密码过期后, 重新登录的时候也会回调onAuthProgress. + * + * 2. 二次认证回调中建议对修改密码进行处理, 对其他不做支持的二次认证需要做一个兜底处理: + * 针对不支持的认证方式做一个提示, 避免出现管理员配置了二次认证后, + * 用户侧收到了二次认证回调, 认证流程未完成但是无任何感知. + */ + if (nextAuthType == SFAuthType.AUTH_TYPE_RENEW_PASSWORD) { + //显示下一步认证UI界面 + showAuthDialog(nextAuthType, message); + return; + } + if (nextAuthType == SFAuthType.AUTH_TYPE_SMS) { + //显示下一步认证UI界面 + showAuthDialog(nextAuthType, message); + return; + } + + showErrorMessage("暂不支持此种认证类型(" + nextAuthType.toString() + ")"); + } + + + //显示“请稍候...”提示框 + public void showWaitingDialog(final boolean isCancelable) { + runOnUiThread(new Runnable() { + @Override + public void run() { + if (mProgressDialog == null || !mProgressDialog.isShowing()) { + mProgressDialog = new ProgressDialog(VpnAuthActivity.this); + mProgressDialog.setProgressStyle(ProgressDialog.STYLE_SPINNER); + mProgressDialog.setTitle(""); + mProgressDialog.setMessage(VpnAuthActivity.this.getString(R.string.waiting)); + mProgressDialog.setCancelable(isCancelable); + mProgressDialog.show(); + } + } + }); + } + + //取消“请稍候...”提示框 + public void dismissWaitingDialog() { + runOnUiThread(new Runnable() { + @Override + public void run() { + if (mProgressDialog != null && mProgressDialog.isShowing()) { + mProgressDialog.dismiss(); + mProgressDialog = null; + } + } + }); + } + + //显示错误提示对话框 + public void showErrorMessage(final String errMsg) { + runOnUiThread(new Runnable() { + @Override + public void run() { + AlertDialog messageDialog = new AlertDialog.Builder(VpnAuthActivity.this) + .setTitle(VpnAuthActivity.this.getString(R.string.info)) + .setMessage(errMsg) + .setIcon(android.R.drawable.ic_dialog_alert) + .setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + dialog.dismiss(); + } + }) + .create(); + + messageDialog.show(); + //确定按钮 + Button btnPos = messageDialog.getButton(DialogInterface.BUTTON_POSITIVE); + //取消按钮 + Button btnNeg = messageDialog.getButton(DialogInterface.BUTTON_NEGATIVE); + btnPos.setTextColor(Color.BLACK); + btnNeg.setTextColor(Color.BLACK); + } + }); + } + + //封装Toast + public void showToast(final String text) { + runOnUiThread(new Runnable() { + @Override + public void run() { + Toast.makeText(VpnAuthActivity.this, text, Toast.LENGTH_SHORT).show(); + } + }); + } + + + //恢复登录信息,重启应用后记录原来的登陆信息 + private void setLoginInfo() { +// SharedPreferences sharedPreferences = getSharedPreferences(Constants.PREF_FILE_NAME, MODE_PRIVATE); +// mServerAddress = sharedPreferences.getString(Constants.KEY_VPN_ADDRESS, mServerAddress); +// mUserName = sharedPreferences.getString(Constants.KEY_USER_NAME, mUserName); +// mUserPassword = sharedPreferences.getString(Constants.KEY_USER_PASSWORD, mUserPassword); + + Intent intent = getIntent(); + mUserName = intent.getStringExtra("usernameVpn"); + mUserPassword = intent.getStringExtra("pwdVpn"); + mServerAddressEditText.setText(mServerAddress); + + if(TextUtils.isEmpty(mUserName)){ + mUserNameEditView.setText(""); + }else{ + mUserNameEditView.setText(mUserName); + } + if(TextUtils.isEmpty(mUserPassword)){ + mUserPasswordEditView.setText(""); + }else{ + mUserPasswordEditView.setText(mUserPassword); + } + } + + //更新保存的登录信息 + private void saveLoginInfo() { + SharedPreferences sharedPreferences = getSharedPreferences(Constants.PREF_FILE_NAME, MODE_PRIVATE); + SharedPreferences.Editor editor = sharedPreferences.edit(); + editor.putString(Constants.KEY_VPN_ADDRESS, mServerAddress); + //保存用户名和密码,真实场景请加密存储 + editor.putString(Constants.KEY_USER_NAME, mUserName); + editor.putString(Constants.KEY_USER_PASSWORD, mUserPassword); + editor.apply(); + } + + /** + * 显示修改密码UI + */ + private void showAuthDialog(final SFAuthType authType, final SFBaseMessage message) { + runOnUiThread(new Runnable() { + @Override + public void run() { + closeDialog(); + //获取认证类型对应的标题 + String dialogTitle = SFDialogHelper.getDialogTitle(authType); + if (TextUtils.isEmpty(dialogTitle)) { + showErrorMessage("暂不支持此种认证类型(" + authType.toString() + ")"); + return; + } + //获取认证类型对应的layout布局 + int viewLayoutId = SFDialogHelper.getAuthDialogViewId(authType); + handlerSecondAuth(dialogTitle, viewLayoutId, authType, message); + } + }); + } + + /** + * 关闭对话框 + */ + private void closeDialog() { + if (mAuthDialog != null && mAuthDialog.isShowing()) { + mAuthDialog.dismiss(); + mAuthDialog = null; + } + } + + /** + * 处理二次验证 + * @param dialogTitle + * @param viewLayoutId + * @param authType + * @param message + */ + private void handlerSecondAuth(String dialogTitle, int viewLayoutId, SFAuthType authType, SFBaseMessage message) { + + //创建认证类型对应的对话框显示视图 + final View dialogView = createDialogView(authType, viewLayoutId, message); + mAuthDialog = new AlertDialog.Builder(VpnAuthActivity.this) + .setTitle(dialogTitle) + .setView(dialogView) + .setCancelable(false) + .setPositiveButton(R.string.str_commit, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + closeDialog(); + //构建辅助认证类型所需参数 + Map authParams = new HashMap(); + String errorMsg = buildAuthParams(authType, dialogView, authParams); + //检查参数构建是否成功 + if (TextUtils.isEmpty(errorMsg)) { + showWaitingDialog(false); + //开始辅助认证 + SFUemSDK.getInstance().doSecondaryAuth(authType, authParams); + } else { + //参数构建失败,进行提示 + showErrorMessage(errorMsg); + } + return; + } + }) + .setNegativeButton(R.string.str_cancel, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + //放弃继续认证 + closeDialog(); + return; + } + }) + .create(); + //显示认证对话框 + mAuthDialog.show(); + + //确定按钮 + Button btnPos = mAuthDialog.getButton(DialogInterface.BUTTON_POSITIVE); + //取消按钮 + Button btnNeg = mAuthDialog.getButton(DialogInterface.BUTTON_NEGATIVE); + btnPos.setTextColor(Color.BLACK); + btnNeg.setTextColor(Color.BLACK); + } + + /** + * 创建认证对话框中间显示的视图 + * + * @param sfAuthType 认证类型 + * @param layoutId 要加载的视图的布局ID + * @param message 认证附加信息 + * @return 认证对话框视图 + */ + private View createDialogView(SFAuthType sfAuthType, int layoutId, final SFBaseMessage message) { + LayoutInflater inflater = getLayoutInflater(); + View dialogView = inflater.inflate(layoutId, null); + switch (sfAuthType) { + case AUTH_TYPE_RENEW_PASSWORD: { + TextView tvPolicy = (TextView) dialogView.findViewById(R.id.tv_policy); + String policy = ""; + //获取密码策略 + if (message instanceof SFChangePswMessage) { + policy = ((SFChangePswMessage) message).policyMsg; + } + if (TextUtils.isEmpty(policy)) { + tvPolicy.setText(R.string.str_no_policy); + } else { + tvPolicy.setText(getString(R.string.str_policy_hint) + "\n" + policy); + } + break; + } + default: + break; + } + + return dialogView; + } + + /** + * 构建辅助认证所需参数 + * + * @param sfAuthType 认证类型 + * @param dialogView 认证视图 + * @param authParams 保存认证参数 + * @return 参数错误信息 + */ + private String buildAuthParams(SFAuthType sfAuthType, View dialogView, Map authParams) { + String errorMsg = ""; + switch (sfAuthType) { + case AUTH_TYPE_RENEW_PASSWORD: { + EditText etNewPwd = dialogView.findViewById(R.id.et_newpwd); + EditText etOldPwd = dialogView.findViewById(R.id.et_oldpwd); + EditText etReNewPwd = dialogView.findViewById(R.id.et_renewpwd); + String oldPwd = etOldPwd.getText().toString(); + String newPwd = etNewPwd.getText().toString(); + String reNewPwd = etReNewPwd.getText().toString(); + + if (TextUtils.isEmpty(newPwd) || TextUtils.isEmpty(reNewPwd)) { + errorMsg = "新密码不能为空"; + break; + } + + if (!newPwd.equals(reNewPwd)) { + errorMsg = "新密码与确认密码不一致"; + break; + } + + authParams.put(SFConstants.AUTH_KEY_RENEW_OLD_PASSWORD, oldPwd); + authParams.put(SFConstants.AUTH_KEY_REWNEW_NEW_PASSWORD, newPwd); + break; + } + case AUTH_TYPE_SMS: { + EditText etSmsCode = dialogView.findViewById(R.id.et_verficationCode); + String smsCode = etSmsCode.getText().toString().trim(); + + if (TextUtils.isEmpty(smsCode)) { + errorMsg = "短信验证码不能为空"; + break; + } + + showLog("-------smsCode---------"); + showLog(smsCode); + authParams.put(SFConstants.AUTH_KEY_SMS, smsCode); + break; + } + default: + errorMsg = "暂不支持的认证类型"; + break; + } + + return errorMsg; + } + + + /** + * 隧道状态变化监听 + * @param curStatus 变化后的隧道状态 + */ + @Override + public void onTunnelStatusChanged(SFTunnelStatus curStatus) { + showLog("onTunnelStatusChanged called: curStatus: " + curStatus); + } + + /** + * 隧道启动完成回调监听,这里需要关注返回的错误码,如果隧道启动失败会在msg里面返回错误码和失败原因 + * @param msg + */ + @Override + public void onTunnelStarted(SFBaseMessage msg) { + //隧道启动完成,跳转认证成功界面,可以开始访问资源 + showLog( "onTunnelStarted, msg: " + msg.mErrStr + ",errCode: " + msg.mErrCode); + + if (msg.mErrCode != 0) { + showLog("start Tunnel failed" + msg.mErrStr); + showLog( "L3VPN隧道启动失败: " + msg.mErrStr); + showToast(msg.mErrStr); + showToast("L3VPN隧道启动失败: " + msg.mErrStr); + return; + } + showLog( "startTunnel finished"); + showLog( "L3VPN隧道启动成功"); + showToast("L3VPN隧道启动成功"); + + } + + /** + * 隧道停止回调 + */ + @Override + public void onTunnelStoped() { + showLog( "onTunnelStoped"); + showLog( "隧道已经关闭"); + showToast("隧道已经关闭"); + } + +} diff --git a/app/src/main/java/com/rehome/zhdcoa/ui/fragment/HomeFragment.kt b/app/src/main/java/com/rehome/zhdcoa/ui/fragment/HomeFragment.kt index 137bbdf..5a2a661 100644 --- a/app/src/main/java/com/rehome/zhdcoa/ui/fragment/HomeFragment.kt +++ b/app/src/main/java/com/rehome/zhdcoa/ui/fragment/HomeFragment.kt @@ -91,6 +91,7 @@ import retrofit2.Call import retrofit2.Response import java.text.SimpleDateFormat import java.util.Calendar +import androidx.core.view.isVisible /** @@ -146,7 +147,10 @@ class HomeFragment : BaseViewBindingFragment() { Handler(Looper.getMainLooper()).postDelayed({ // 在指定时间后要执行的代码 //隐藏掉 本平台为非涉密平台,严禁处理、传输国家秘密、工作秘密、商业秘密、敏感信息 //showLog("隐藏掉 本平台为非涉密平台,严禁处理、传输国家秘密、工作秘密、商业秘密、敏感信息") - binding.llTip.visibility=View.GONE; + if(binding!=null){ + binding.llTip.visibility=View.GONE; + } + }, 5000) } } @@ -170,13 +174,15 @@ class HomeFragment : BaseViewBindingFragment() { override fun onResume() { super.onResume() showLog("onResume") - if (this.isAdded && this.isVisible&&binding.llTip.visibility==View.VISIBLE) { + if (this.isAdded && this.isVisible) { showLog("this.isAdded && this.isVisible&&binding.llTip.visibility==View.VISIBLE") // Fragment存在且可见,可以安全访问 Handler(Looper.getMainLooper()).postDelayed({ // 在指定时间后要执行的代码 //隐藏掉 本平台为非涉密平台,严禁处理、传输国家秘密、工作秘密、商业秘密、敏感信息 //showLog("隐藏掉 本平台为非涉密平台,严禁处理、传输国家秘密、工作秘密、商业秘密、敏感信息") - binding.llTip.visibility=View.GONE; + if(binding!=null&&binding.llTip.isVisible){ + binding.llTip.visibility=View.GONE + } }, 5000) } } diff --git a/app/src/main/java/com/rehome/zhdcoa/utils/DpPxSpTransformUtil.java b/app/src/main/java/com/rehome/zhdcoa/utils/DpPxSpTransformUtil.java new file mode 100644 index 0000000..792fb9b --- /dev/null +++ b/app/src/main/java/com/rehome/zhdcoa/utils/DpPxSpTransformUtil.java @@ -0,0 +1,47 @@ +package com.rehome.zhdcoa.utils; + +/** + * @ProjectName: SDKDemo + * @Package: com.sangfor.sdkdemo.utils + * @ClassName: DpPxSpTransformUtil + * @Description: java类作用描述 + * @Author: yanghua + * @CreateDate: 2022/9/21 下午6:11 + */ +public class DpPxSpTransformUtil { + private static float scale; + /** + * 初始化获得屏幕密度 + * @return 屏幕密度 + */ + public static void init(float scale){ + DpPxSpTransformUtil.scale=scale; + } + /** + * 根据手机的分辨率从 dp 的单位 转成为 px(像素) + */ + public static int dip2px(float dpValue) { + return (int)(dpValue * scale + 0.5f); + } + /** + * 根据手机的分辨率从 px(像素) 的单位 转成为 dp + */ + public static int px2dip(float pxValue) { + return (int)(pxValue / scale + 0.5f); + } + /** + * 将px值转换为sp值,保证文字大小不变 + * @return + */ + public static int px2sp(float pxValue) { + return (int) (pxValue / scale + 0.5f); + } + + /** + * 将sp值转换为px值,保证文字大小不变 + * @return + */ + public static int sp2px(float spValue) { + return (int) (spValue * scale + 0.5f); + } +} diff --git a/app/src/main/java/com/rehome/zhdcoa/vpn/AppInfoUtils.java b/app/src/main/java/com/rehome/zhdcoa/vpn/AppInfoUtils.java new file mode 100755 index 0000000..9bc3277 --- /dev/null +++ b/app/src/main/java/com/rehome/zhdcoa/vpn/AppInfoUtils.java @@ -0,0 +1,67 @@ +package com.rehome.zhdcoa.vpn; + +import android.app.Activity; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.text.TextUtils; +import android.util.Log; + +/** + * 获取app信息的公共类 + */ +public class AppInfoUtils { + + private static String TAG = "AppInfoUtils"; + + /** + * 获取当前应用名称 + * + * @param activity + * @return + */ + public static String getApplicationName(Activity activity) { + PackageManager packageManager; + ApplicationInfo applicationInfo; + try { + packageManager = activity.getApplicationContext().getPackageManager(); + applicationInfo = packageManager.getApplicationInfo(activity.getPackageName(), 0); + } catch (PackageManager.NameNotFoundException e) { + Log.i(TAG,"PackageManager.NameNotFoundException"); + return ""; + } + String applicationName = (String) packageManager.getApplicationLabel(applicationInfo); + if (TextUtils.isEmpty(applicationName)) { + Log.i(TAG,"applicationName is Empty"); + return ""; + } else { + return applicationName; + } + } + + /** + * 获取当前应用名称 + * + * @param activity + * @return + */ + public static String getApplicationName(Activity activity, String packAgeName) { + PackageManager packageManager; + ApplicationInfo applicationInfo; + try { + packageManager = activity.getApplicationContext().getPackageManager(); + applicationInfo = packageManager.getApplicationInfo(packAgeName, 0); + } catch (PackageManager.NameNotFoundException e) { + Log.i(TAG,"PackageManager.NameNotFoundException"); + return ""; + } + + String applicationName = (String) packageManager.getApplicationLabel(applicationInfo); + if (TextUtils.isEmpty(applicationName)) { + Log.i(TAG,"applicationName is Empty"); + return ""; + } else { + return applicationName; + } + } + +} diff --git a/app/src/main/java/com/rehome/zhdcoa/vpn/AssetFileUtil.java b/app/src/main/java/com/rehome/zhdcoa/vpn/AssetFileUtil.java new file mode 100755 index 0000000..628e02b --- /dev/null +++ b/app/src/main/java/com/rehome/zhdcoa/vpn/AssetFileUtil.java @@ -0,0 +1,47 @@ +package com.rehome.zhdcoa.vpn; + +import android.content.Context; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; + +public class AssetFileUtil { + + /** + * 获取assert 目录下某个文件内容 + * @param context + * @param fileName + * @return + */ + public static String getAssertFileContent(Context context, String fileName){ + StringBuffer stringBuffer = new StringBuffer(); + + InputStream inputStream = null; + try { + inputStream = context.getAssets().open(fileName); + InputStreamReader inputreader = new InputStreamReader(inputStream); + BufferedReader buffreader = new BufferedReader(inputreader); + String line; + // 分行读取 + while ((line = buffreader.readLine()) != null) { + stringBuffer.append(line); + } + buffreader.close(); + inputreader.close(); + } catch (IOException e) { + e.printStackTrace(); + } + if(inputStream != null){ + try { + inputStream.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + return stringBuffer.toString(); + } + +} diff --git a/app/src/main/java/com/rehome/zhdcoa/vpn/AuthSessionDialog.java b/app/src/main/java/com/rehome/zhdcoa/vpn/AuthSessionDialog.java new file mode 100755 index 0000000..00ac223 --- /dev/null +++ b/app/src/main/java/com/rehome/zhdcoa/vpn/AuthSessionDialog.java @@ -0,0 +1,89 @@ +package com.rehome.zhdcoa.vpn; + +import android.app.Activity; +import android.app.AlertDialog; +import android.content.DialogInterface; +import com.rehome.zhdcoa.R; +import com.sangfor.sdk.entry.SFLaunchInfo; + + + +/** + * 是否给子应用授权弹窗 + */ +public class AuthSessionDialog { + + private Activity mActivity; + + private DialogClickListener mClickListener; + + private AlertDialog mDialog; + private SFLaunchInfo mSFLaunchInfo; + + public void setClickListener(DialogClickListener clickListener) { + mClickListener = clickListener; + } + + public AuthSessionDialog(Activity activity, SFLaunchInfo launchInfo) { + mActivity = activity; + mSFLaunchInfo = launchInfo; + } + + public void show() { + if (mDialog == null) { + String hostAppName = AppInfoUtils.getApplicationName(mActivity); + String subAppName = AppInfoUtils.getApplicationName(mActivity, mSFLaunchInfo.getPackageName()); + String dialogMessage = String.format(mActivity.getString(R.string.request_session_can_you_agree), subAppName, hostAppName); + + mDialog = new AlertDialog.Builder(mActivity) + .setMessage(dialogMessage) + .setCancelable(false) + .setPositiveButton(mActivity.getString(R.string.agree), new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + dismiss(); + + if (mClickListener != null) { + mClickListener.onPositiveClick(mSFLaunchInfo); + } + } + }) + .setNegativeButton(mActivity.getString(R.string.disagree), new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + dismiss(); + + if (mClickListener != null) { + mClickListener.onNegativeClick(mSFLaunchInfo); + } + } + }) + .create(); + } + + mDialog.show(); + } + + public void dismiss() { + if (mDialog != null && mDialog.isShowing()) { + mDialog.dismiss(); + } + } + + public boolean isShowing() { + if (mDialog != null) { + return mDialog.isShowing(); + } + + return false; + } + + public SFLaunchInfo getSFLaunchInfo() { + return mSFLaunchInfo; + } + + public interface DialogClickListener { + void onPositiveClick(SFLaunchInfo launchInfo); + void onNegativeClick(SFLaunchInfo launchInfo); + } +} diff --git a/app/src/main/java/com/rehome/zhdcoa/vpn/CertUtils.java b/app/src/main/java/com/rehome/zhdcoa/vpn/CertUtils.java new file mode 100755 index 0000000..4b63629 --- /dev/null +++ b/app/src/main/java/com/rehome/zhdcoa/vpn/CertUtils.java @@ -0,0 +1,212 @@ +package com.rehome.zhdcoa.vpn; + +import android.annotation.SuppressLint; +import android.annotation.TargetApi; +import android.content.ContentResolver; +import android.content.ContentUris; +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; +import android.os.Build; +import android.os.Environment; +import android.os.FileUtils; +import android.provider.DocumentsContract; +import android.provider.MediaStore; +import android.provider.OpenableColumns; + +import androidx.annotation.RequiresApi; + +import com.sangfor.sdk.utils.SFLogN; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; + +/** + * Created by sangfor on 17-9-28. + */ +public class CertUtils { + private static final String TAG = "CertUtils"; + + /** + * 根据Uri的不同Scheme解析出在本机的路径 + * @param context + * @param uri + * @return Uri的真实路径 + */ + @TargetApi(19) + public static String fromUriGetRealPath(Context context, Uri uri) { + if (uri == null) { + return ""; + } + + final boolean isKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT; + // DocumentProvider + if (isKitKat && Build.VERSION.SDK_INT < Build.VERSION_CODES.Q && DocumentsContract.isDocumentUri(context, uri) ) { + // ExternalStorageProvider + if (isExternalStorageDocument(uri)) { + final String docId = DocumentsContract.getDocumentId(uri); + final String[] split = docId.split(":"); + final String type = split[0]; + + if ("primary".equalsIgnoreCase(type)) { + return Environment.getExternalStorageDirectory() + "/" + split[1]; + } + + // TODO handle non-primary volumes + } + // DownloadsProvider + else if (isDownloadsDocument(uri)) { + final String id = DocumentsContract.getDocumentId(uri); + final Uri contentUri = ContentUris.withAppendedId(Uri.parse("content://downloads/public_downloads"), Long.valueOf(id)); + + return getDataColumn(context, contentUri, null, null); + } + // MediaProvider + else if (isMediaDocument(uri)) { + final String docId = DocumentsContract.getDocumentId(uri); + final String[] split = docId.split(":"); + final String type = split[0]; + + Uri contentUri = null; + if ("image".equals(type)) { + contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI; + } else if ("video".equals(type)) { + contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI; + } else if ("audio".equals(type)) { + contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI; + } + + final String selection = "_id=?"; + final String[] selectionArgs = new String[]{split[1]}; + + return getDataColumn(context, contentUri, selection, selectionArgs); + } + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + return uriToFileApiQ(context, uri); + } + // MediaStore (and general) + else if ("content".equalsIgnoreCase(uri.getScheme())) { + + // Return the remote address + if (isGooglePhotosUri(uri)) + return uri.getLastPathSegment(); + SFLogN.info(TAG, "uri authority is "+uri.getAuthority()); + if (isHuaWeiUri(uri)) { + String uriPath = uri.getPath(); + // content://com.huawei.filemanager.share.fileprovider/root/storage/emulated/0/hbz1.p12 + if (uriPath != null && uriPath.startsWith("/root")) { + return uriPath.replaceAll("/root", ""); + } + } + + return getDataColumn(context, uri, null, null); + } + // File + else if ("file".equalsIgnoreCase(uri.getScheme())) { + return uri.getPath(); + } + + return ""; + } + /** + * Android 10 以上适配 + * @param context + * @param uri + * @return + */ + @RequiresApi(api = Build.VERSION_CODES.Q) + private static String uriToFileApiQ(Context context, Uri uri) { + File file = null; + //android10以上转换 + if (uri.getScheme().equals(ContentResolver.SCHEME_FILE)) { + file = new File(uri.getPath()); + } else if (uri.getScheme().equals(ContentResolver.SCHEME_CONTENT)) { + //把文件复制到沙盒目录 + ContentResolver contentResolver = context.getContentResolver(); + Cursor cursor = contentResolver.query(uri, null, null, null, null); + if (cursor.moveToFirst()) { + @SuppressLint("Range") + String displayName = cursor.getString(cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)); + try { + InputStream is = contentResolver.openInputStream(uri); + File file1 = new File(context.getExternalCacheDir().getAbsolutePath()+"/"+System.currentTimeMillis()); + if (!file1.exists()) + { + file1.mkdir(); + } + File cache = new File(file1.getPath(), displayName); + FileOutputStream fos = new FileOutputStream(cache); + FileUtils.copy(is, fos); + file = cache; + fos.close(); + is.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + return file.getAbsolutePath(); + } + + public static String getDataColumn(Context context, Uri uri, String selection, String[] selectionArgs) { + + Cursor cursor = null; + final String column = "_data"; + final String[] projection = {column}; + + try { + cursor = context.getContentResolver().query(uri, null, selection, selectionArgs, null); + if (cursor != null && cursor.moveToFirst()) { + final int index = cursor.getColumnIndexOrThrow(column); + return cursor.getString(index); + } + } finally { + if (cursor != null) + cursor.close(); + } + return ""; + } + + /** + * @param uri The Uri to check. + * @return Whether the Uri authority is ExternalStorageProvider. + */ + public static boolean isExternalStorageDocument(Uri uri) { + return "com.android.externalstorage.documents".equals(uri.getAuthority()); + } + + /** + * @param uri The Uri to check. + * @return Whether the Uri authority is DownloadsProvider. + */ + public static boolean isDownloadsDocument(Uri uri) { + return "com.android.providers.downloads.documents".equals(uri.getAuthority()); + } + + /** + * @param uri The Uri to check. + * @return Whether the Uri authority is MediaProvider. + */ + public static boolean isMediaDocument(Uri uri) { + return "com.android.providers.media.documents".equals(uri.getAuthority()); + } + + /** + * @param uri The Uri to check. + * @return Whether the Uri authority is Google Photos. + */ + public static boolean isGooglePhotosUri(Uri uri) { + return "com.google.android.apps.photos.content".equals(uri.getAuthority()); + } + + /** + * 适配 华为手机 + * @param uri + * @return + */ + public static boolean isHuaWeiUri(Uri uri) { + return "com.huawei.filemanager.share.fileprovider".equals(uri.getAuthority()); + } +} diff --git a/app/src/main/java/com/rehome/zhdcoa/vpn/Constants.java b/app/src/main/java/com/rehome/zhdcoa/vpn/Constants.java new file mode 100755 index 0000000..936930e --- /dev/null +++ b/app/src/main/java/com/rehome/zhdcoa/vpn/Constants.java @@ -0,0 +1,12 @@ +package com.rehome.zhdcoa.vpn; + +public class Constants { + public final static String PREF_FILE_NAME = "loginConfig"; + public final static String KEY_VPN_ADDRESS = "vpnAddress"; + public final static String KEY_USER_NAME = "userName"; + public final static String KEY_USER_PASSWORD = "userPassword"; + public final static String KEY_CERT_PATH = "certPath"; + public final static String KEY_CERT_PASSWORD = "certPassword"; + public final static String KEY_PHONE_NUMBER = "phoneNumber"; + public final static String KEY_SUB_APP_PKG_NAME = "subAppPkgName"; +} diff --git a/app/src/main/java/com/rehome/zhdcoa/vpn/GlobalListenerManager.java b/app/src/main/java/com/rehome/zhdcoa/vpn/GlobalListenerManager.java new file mode 100644 index 0000000..7747529 --- /dev/null +++ b/app/src/main/java/com/rehome/zhdcoa/vpn/GlobalListenerManager.java @@ -0,0 +1,140 @@ +package com.rehome.zhdcoa.vpn; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.widget.Toast; +import com.sangfor.sdk.SFUemSDK; +import com.sangfor.sdk.base.SFAuthStatus; +import com.sangfor.sdk.base.SFBaseMessage; +import com.sangfor.sdk.base.SFLaunchReason; +import com.sangfor.sdk.base.SFLogoutListener; +import com.sangfor.sdk.base.SFLogoutType; +import com.sangfor.sdk.entry.SFLaunchEntry; +import com.sangfor.sdk.entry.SFLaunchInfo; +import com.sangfor.sdk.utils.SFLogN; + + +public class GlobalListenerManager { + private static final String TAG = "GlobalListenerManager"; + + private Context mAppContext; + + private AuthSessionDialog mAuthSessionDialog = null; + + public static GlobalListenerManager getInstance() { + return SingletonHolder.INSTANCE; + } + + public void init(Context context) { + mAppContext = context.getApplicationContext() == null ? context : context.getApplicationContext(); + addSFListener(); + } + + /** + * 增加sdk事件监听 + */ + private void addSFListener() { + + /** + * 注销事件监听回调,推荐在Application里面监听, 避免出现认证开始了还未监听的问题 + */ + SFUemSDK.getInstance().registerLogoutListener(new SFLogoutListener() { + @Override + public void onLogout(SFLogoutType type, SFBaseMessage message) { + SFLogN.info(TAG, "onLogout, message: " + message); + /** + * 收到注销事件,demo的处理是跳转到认证前的入口页面 + */ +// Intent intent = new Intent(mAppContext, EntryActivity.class); +// intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); +// mAppContext.startActivity(intent); + } + }); + + /** + * 主从场景需要 + * 子应用拉起事件监听回调,存在主从场景时才需要使用,推荐在Application里面监听,避免出现子应用拉起事件发生还未监听的问题 + */ + SFUemSDK.getInstance().getSFLaunch().setAppLaunchListener(new SFLaunchEntry.SFAppLaunchListener() { + @Override + public void onAppLaunched(SFLaunchInfo launchInfo) { + SFLogN.info(TAG, "setAppLaunchListener onAppLaunched, launchInfo is :" + launchInfo); + + if (!SFLaunchEntry.isSubApp()) { + + /** + * 只处子应用拉起获取登录凭据事件 + */ + if (launchInfo.getLaunchReason() == SFLaunchReason.Launch_HOSTAPP_AUTH_AUTHORIZATION) { + + /** + * 如果主应用当前是未认证成功状态,无法进行登陆授权,主应用需要先进行认证 + */ + if (SFUemSDK.getInstance().getAuthStatus() != SFAuthStatus.AuthStatusAuthOk) { + SFLogN.info(TAG, "host app not auth ok, igore it"); + Toast.makeText(mAppContext, "主应用尚未认证成功,请先认证", Toast.LENGTH_SHORT).show(); + return; + } + + String subAppPkgName = launchInfo.getSubAppInfo().getSubAppPackageName(); + + if (!SFUemSDK.getInstance().getSFLaunch().checkAppAuthorized(launchInfo)) { + SFLogN.info(TAG, "sub app not Authorized, igore it"); + Toast.makeText(mAppContext, "管理员未授权该应用", Toast.LENGTH_SHORT).show(); + return; + } + + /** + * sdk内部会确保主应用进入前台后才回调onAppLaunched,且会在launchInfo中提供当前主应用前台的activity获取方式 + * ,方便用户可以直接使用此activity做弹框交互 + */ + Activity activity = launchInfo.getForegroundActivity(); + + /** + * 这里弹框让用户确认是否要给子应用授权,如果需求上不需要做弹框让用户确认,也可以直接在此处调用 + * launcherSubApp拉回子应用 + */ + showAuthSessionDialog(activity, launchInfo); + + } + } + } + }); + } + + private void showAuthSessionDialog(Activity activity, SFLaunchInfo sfLaunchInfo) { + if (mAuthSessionDialog != null) { + mAuthSessionDialog.dismiss(); + mAuthSessionDialog = null; + } + + mAuthSessionDialog = new AuthSessionDialog(activity, sfLaunchInfo); + mAuthSessionDialog.setClickListener(new AuthSessionDialog.DialogClickListener() { + @Override + public void onPositiveClick(SFLaunchInfo launchInfo) { + SFLogN.info(TAG, "AuthSessionDialog positiveClick."); + /** + * 拉回到子应用进行授权 + */ + SubAppManager.getInstance().launcherSubApp(mAppContext, launchInfo); + } + + @Override + public void onNegativeClick(SFLaunchInfo launchInfo) { + SFLogN.info(TAG, "AuthSessionDialog negativeClick."); + } + }); + + SFLogN.info(TAG, "showAuthSessionDialog."); + mAuthSessionDialog.show(); + } + + private GlobalListenerManager() { + + } + + private final static class SingletonHolder { + private static final GlobalListenerManager INSTANCE = new GlobalListenerManager(); + } +} diff --git a/app/src/main/java/com/rehome/zhdcoa/vpn/PermissionUtil.java b/app/src/main/java/com/rehome/zhdcoa/vpn/PermissionUtil.java new file mode 100755 index 0000000..a1c243d --- /dev/null +++ b/app/src/main/java/com/rehome/zhdcoa/vpn/PermissionUtil.java @@ -0,0 +1,88 @@ +package com.rehome.zhdcoa.vpn; + +import android.Manifest; +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.net.Uri; +import android.os.Build; +import android.os.Environment; +import android.provider.Settings; +import android.widget.Toast; + +import androidx.core.app.ActivityCompat; +import androidx.core.content.ContextCompat; + +import com.sangfor.sdk.utils.SFLogN; + +import java.util.ArrayList; +import java.util.List; + +public class PermissionUtil { + private static final String TAG = "PermissionUtil"; + + public static boolean isNeedRequestSDCardPermission(Context context) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { + return false; + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + // 先判断有没有权限 + //context.checkSelfPermission(Manifest.permission.MANAGE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED + if (!Environment.isExternalStorageManager()) { + return true; + } + } else { + if (context.checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { + return true; + } + if (context.checkSelfPermission(Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { + return true; + } + } + return false; + } + + + public static void requestSDCardPermissions(Activity activity, int requestCode) { + if (activity == null) { + return; + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + //android 11 + Intent intent = new Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION); + intent.setData(Uri.parse("package:" + activity.getPackageName())); + activity.startActivityForResult(intent, requestCode); + Toast.makeText(activity, "请授予所有文件管理权限~", Toast.LENGTH_SHORT).show(); + } else { + List permissionsList = new ArrayList(); + if (ContextCompat.checkSelfPermission(activity.getBaseContext(), Manifest.permission.WRITE_EXTERNAL_STORAGE) + != PackageManager.PERMISSION_GRANTED) { + SFLogN.info(TAG,"No WRITE_EXTERNAL_STORAGE permission"); + permissionsList.add(Manifest.permission.WRITE_EXTERNAL_STORAGE); + } + if (ContextCompat.checkSelfPermission(activity.getBaseContext(), Manifest.permission.READ_EXTERNAL_STORAGE) + != PackageManager.PERMISSION_GRANTED) { + SFLogN.info(TAG,"No READ_EXTERNAL_STORAGE permission"); + permissionsList.add(Manifest.permission.READ_EXTERNAL_STORAGE); + } + if (permissionsList.isEmpty()) { + return; + } + String[] permissions = permissionsList.toArray(new String[permissionsList.size()]); + ActivityCompat.requestPermissions(activity, permissions, requestCode); + } + + } + + /** + * 跳转至系统设置内的应用详情页面 + * @param context + */ + public static void gotoAppPermissionManageActivity(Context context) { + Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); + intent.setData(Uri.parse("package:" + context.getPackageName())); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + context.startActivity(intent); + } +} diff --git a/app/src/main/java/com/rehome/zhdcoa/vpn/SFDialogHelper.java b/app/src/main/java/com/rehome/zhdcoa/vpn/SFDialogHelper.java new file mode 100755 index 0000000..e3a1ef7 --- /dev/null +++ b/app/src/main/java/com/rehome/zhdcoa/vpn/SFDialogHelper.java @@ -0,0 +1,68 @@ +package com.rehome.zhdcoa.vpn; + +import com.sangfor.sdk.SFMobileSecuritySDK; +import com.sangfor.sdk.SFUemSDK; +import com.sangfor.sdk.base.SFAuthType; +import com.rehome.zhdcoa.R; + +public class SFDialogHelper { + /** + * 对话框标题 + * + * @param authType 认证类型 + * @return 对话框标题 + */ + public static String getDialogTitle(SFAuthType authType) { + switch (authType) { + case AUTH_TYPE_PASSWORD: + return "密码认证"; + case AUTH_TYPE_CERTIFICATE: + return "证书认证"; + case AUTH_TYPE_SMS: + case AUTH_TYPE_PRIMARY_SMS: + return "短信认证"; + case AUTH_TYPE_RADIUS: + return "挑战认证"; + case AUTH_TYPE_TOKEN: + case AUTH_TYPE_TOKEN_RADIUS: + case AUTH_TYPE_TOKEN_TOTP: + case AUTH_TYPE_TOKEN_HTTPS: + return "令牌认证"; + case AUTH_TYPE_RAND: + return "图形校验码"; + case AUTH_TYPE_RENEW_PASSWORD: + return "修改密码"; + default: + return ""; + } + } + + public static int getAuthDialogViewId(SFAuthType authType) { + switch (authType) { + case AUTH_TYPE_PASSWORD: + return R.layout.dialog_pwd; + case AUTH_TYPE_CERTIFICATE: + return R.layout.dialog_certificate; + case AUTH_TYPE_SMS: + case AUTH_TYPE_PRIMARY_SMS: + return R.layout.dialog_sms; + case AUTH_TYPE_RADIUS: + return R.layout.dialog_challenge; + case AUTH_TYPE_TOKEN: + case AUTH_TYPE_TOKEN_RADIUS: + case AUTH_TYPE_TOKEN_TOTP: + case AUTH_TYPE_TOKEN_HTTPS: + return R.layout.dialog_token; + case AUTH_TYPE_RAND: + if (SFUemSDK.getInstance().isSDPServce()) { + //sdp服务器,图形验证码返回当前layout + return R.layout.dialog_graph_check_sdp; + } + return R.layout.dialog_graph_check; + case AUTH_TYPE_RENEW_PASSWORD: + return R.layout.dialog_force_update_pwd; + default: + return -1; + } + } +} diff --git a/app/src/main/java/com/rehome/zhdcoa/vpn/SignatureUtils.java b/app/src/main/java/com/rehome/zhdcoa/vpn/SignatureUtils.java new file mode 100755 index 0000000..4e118ac --- /dev/null +++ b/app/src/main/java/com/rehome/zhdcoa/vpn/SignatureUtils.java @@ -0,0 +1,74 @@ +package com.rehome.zhdcoa.vpn; + +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.Signature; +import android.text.TextUtils; + +import com.sangfor.sdk.utils.SFLogN; + +import java.io.ByteArrayInputStream; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class SignatureUtils { + private static final String TAG = "SignatureUtils"; + + /** + * 获取应用中的公钥信息 + * @param appInfo + * @return 应用中公钥的字符串形式 + */ + public static String getPubKeyInfo(ApplicationInfo appInfo, Context context) { + PackageManager mPackageManager = context.getPackageManager(); + if (mPackageManager != null){ + try { + PackageInfo packageInfo = mPackageManager.getPackageInfo(appInfo.packageName, mPackageManager.GET_SIGNATURES); + Signature[] signatures = packageInfo.signatures; + Signature signature = signatures[0]; + byte[] signatureBytes = signature.toByteArray(); + CertificateFactory certFactory = CertificateFactory.getInstance("X.509"); + X509Certificate cert = (X509Certificate) certFactory.generateCertificate(new ByteArrayInputStream(signatureBytes)); + String pubKeyStr = cert.getPublicKey().toString(); + return parsePubKey(pubKeyStr); + } catch (PackageManager.NameNotFoundException e) { + SFLogN.error(TAG, "failed to get packageInfo ", e); + } catch (CertificateException e) { + SFLogN.error(TAG, "failed to get cert", e); + } + } + return null; + } + + /** + * 根据解析安装apk的信息得到公钥信息 + * OpenSSLRSAPublicKey{modulus=98cfceafd123bc2d480af0bf31d41ca542736f8353031b01efad25dcef2dd566491635f23c0fbc950c93624dff4436c0910b0cdfe85bf6a86bcd7e42d790110ef4eec3eb8d3fb279734e71c6afd715f33712207101fa40ed0d196ad7c499bbc55d096de61f94117efaea996eed3a3a7c5bb875164bcb890439f6ccd2c0b2acb5,publicExponent=10001}"; + * @param info + * @return + */ + private static String parsePubKey(String info) { + if (TextUtils.isEmpty(info)) { + return null; + } + String indexStr = "modulus="; + int index = info.indexOf(indexStr); + if (index != -1) { + int startIndex = index + indexStr.length(); + indexStr = info.substring(startIndex); + String pubkey = indexStr.split(",")[0]; + //因为解析出来的公钥信息中可能含有空格,换行等干扰信息,因此需要对其进行去杂处理,方便后续的字符串比较 + if (pubkey != null) { + Pattern pattern = Pattern.compile("\\s*|\t|\r|\n"); + Matcher matcher = pattern.matcher(pubkey); + pubkey = matcher.replaceAll(""); + } + return pubkey; + } + return null; + } +} diff --git a/app/src/main/java/com/rehome/zhdcoa/vpn/SubAppManager.java b/app/src/main/java/com/rehome/zhdcoa/vpn/SubAppManager.java new file mode 100755 index 0000000..f13ba9d --- /dev/null +++ b/app/src/main/java/com/rehome/zhdcoa/vpn/SubAppManager.java @@ -0,0 +1,69 @@ +package com.rehome.zhdcoa.vpn; + +import android.content.Context; + +import com.sangfor.sdk.SFSecuritySDKFactory; +import com.sangfor.sdk.SFUemSDK; +import com.sangfor.sdk.base.SFLaunchReason; +import com.sangfor.sdk.entry.SFLaunchInfo; +import com.sangfor.sdk.utils.SFLogN; + +/** + * 子应用管理类,保持子应用传递过来的信息,主要方便主应用拉起子应用 + */ +public class SubAppManager { + + private static final String TAG = "SubAppManager"; + + private SFLaunchInfo mSubAppLaunchInfo; + private static volatile SubAppManager sInstance; + + private SubAppManager() { + } + + public static SubAppManager getInstance() { + if (sInstance == null) { + synchronized (SubAppManager.class) { + if (sInstance == null) { + sInstance = new SubAppManager(); + } + } + } + + return sInstance; + } + + public void setSubAppLaunchInfo(SFLaunchInfo subAppLaunchInfo) { + this.mSubAppLaunchInfo = subAppLaunchInfo; + } + + public SFLaunchInfo getSubAppLaunchInfo() { + return mSubAppLaunchInfo; + } + + /** + * 拉回子应用 + * @param context + */ + public void launcherSubApp(Context context, SFLaunchInfo launchInfo) { + if (context == null) { + SFLogN.error2(TAG, "cannot launcherSubApp", "context=null"); + return; + } + SFLogN.info(TAG, "will launcherSubApp..." + launchInfo.getSubAppInfo().getSubAppPackageName()); + if (launchInfo != null) { + SFLogN.info(TAG, "will start sub app..."); + SFUemSDK.getInstance().getSFLaunch().launchSubApp(context, launchInfo); + } else { + SFLogN.warn2(TAG, "can not start sub app...", "launchInfo is null"); + } + + // 子应用被拉起后,表示一个流程执行完成,需要清空数据(主应用被子应用拉起-->主应用拉回子应用) + clearCache(); + } + + public void clearCache() { + this.mSubAppLaunchInfo = null; + } + +} diff --git a/app/src/main/java/com/rehome/zhdcoa/weiget/RandCodeCircleView.java b/app/src/main/java/com/rehome/zhdcoa/weiget/RandCodeCircleView.java new file mode 100644 index 0000000..249ec34 --- /dev/null +++ b/app/src/main/java/com/rehome/zhdcoa/weiget/RandCodeCircleView.java @@ -0,0 +1,53 @@ +package com.rehome.zhdcoa.weiget; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.inputmethod.InputMethodManager; +import android.widget.RelativeLayout; +import android.widget.TextView; + +import com.rehome.zhdcoa.R; + +/** + * SDP图形验证码点击圆圈自定义view + * author yanghua + * date 20220915 + */ +public class RandCodeCircleView extends RelativeLayout { + + private Context context; + private View view; + private SangforTextView tvNum; + private RandCodeLayout.ClickPoint mPoint; + + public RandCodeCircleView(Context context) { + super(context); + + } + + public RandCodeCircleView(Context context, RandCodeLayout.ClickPoint point) { + super(context); + this.context = context; + this.mPoint = point; + initViews(); + } + + /** + * 初始化视图 + **/ + protected void initViews() { + view = LayoutInflater.from(context).inflate(R.layout.randcode_cricle_view, this, true); + tvNum = view.findViewById(R.id.tvNum); + tvNum.setText(mPoint.getNum() + ""); + } + + public int getNum() { + return mPoint.getNum(); + } + + public void setPoint(RandCodeLayout.ClickPoint point) { + this.mPoint = point; + tvNum.setText(mPoint.getNum() + ""); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/rehome/zhdcoa/weiget/RandCodeLayout.java b/app/src/main/java/com/rehome/zhdcoa/weiget/RandCodeLayout.java new file mode 100644 index 0000000..ed3e578 --- /dev/null +++ b/app/src/main/java/com/rehome/zhdcoa/weiget/RandCodeLayout.java @@ -0,0 +1,358 @@ +package com.rehome.zhdcoa.weiget; + + +import android.annotation.SuppressLint; +import android.content.Context; +import android.graphics.Rect; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.View; +import android.view.View.OnTouchListener; +import android.widget.LinearLayout; +import android.widget.RelativeLayout; + +import com.sangfor.sdk.base.SFAuthType; +import com.sangfor.sdk.utils.SFLogN; + +import java.util.ArrayList; + + +/** + * SDP图形验证码容器类自定义控件 + * author yanghua + * date 20220915 + */ +@SuppressLint("NewApi") +public class RandCodeLayout extends RelativeLayout implements OnTouchListener { + + private final String TAG = "RandCodeLayout"; + private final int LIMT_NUM = 3; + private Context mContext; + private RandCodeCircleView mTouchView; + private int mStartX; + private int mStartY; + private int mChildHeight; + private int mChildWidth; + private int mlayoutWith; + private int mlayoutHeight; + private String user_position; + private ArrayList mPoints; + private OnFinishClickListener mFinishClickCallback; + private boolean isCheckCodeing = false; + + public void setOnFinishClickListener(OnFinishClickListener callback) { + this.mFinishClickCallback = callback; + } + + public RandCodeLayout(Context context) { + super(context, null); + } + + public RandCodeLayout(Context context, AttributeSet attrs) { + super(context, attrs); + this.mContext = context; + init(); + } + + private void init() { + this.setOnTouchListener(this); + mPoints = new ArrayList(); + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + super.onSizeChanged(w, h, oldw, oldh); + SFLogN.info(TAG, "onSizeChanged:" + w + "/" + h); + //缓存原来的大小 + float cacheOriWith = mlayoutWith; + float cacheOriHeight = mlayoutHeight; + //保存新view大小 + mlayoutWith = w; + mlayoutHeight = h; + + ArrayList cachePoints = new ArrayList<>(); + if (mPoints == null || mPoints.size() == 0) { + return; + } + + //处理数据,计算新的布局,点的位置 + int size = mPoints.size(); + for (int i = 0; i < size; i++) { + ClickPoint clickPoint = mPoints.get(i); + int cacheX = (int) ((clickPoint.clickX/cacheOriWith) * w); + int cacheY = (int) ((clickPoint.clickY/cacheOriHeight) * h); + cachePoints.add(new ClickPoint(cacheX,cacheY, i+1)); + } + + //清除数据 + clearView(); + + //重新添加数据 + if (cachePoints.size() > 0) { + for (int i = 0; i < size; i++) { + ClickPoint cachePoint = cachePoints.get(i); + addPopup(cachePoint.clickX, cachePoint.clickY); + } + } + } + + //开始的位置小于结束的位置 向左滑动 + @Override + public boolean onTouch(View v, MotionEvent event) { + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + break; + case MotionEvent.ACTION_MOVE: + break; + case MotionEvent.ACTION_UP: + if (isCheckCodeing) { + SFLogN.info(TAG, "onTouch is checking" ); + break; + } + mStartX = (int) event.getX(); + mStartY = (int) event.getY(); + if (!hasView(mStartX, mStartY) && isClickableRect(mStartX, mStartY)) { + //如果点击位置没有点击过,并且点击的位置在可点击的返回内 + addPopup(mStartX, mStartY); + } else { + //点击的位置包含已经存在的view,则移除当前的view + removePopup(); + } + break; + + } + return true; + } + + /** + * 删除标记 + */ + private void removePopup() { + if (mTouchView != null) { + int num = mTouchView.getNum(); + removeAllViews(); + mTouchView = null; + user_position = ""; + + //移除数据 + for (int i = 0; i < mPoints.size(); i++) { + ClickPoint point = mPoints.get(i); + if (point.getNum() == num) { + mPoints.remove(i); + } + } + + //更新数据 + for (int i = 0; i < mPoints.size(); i++) { + mPoints.get(i).setNum(i + 1); + } + + //更新UI + for (int i = 0; i < mPoints.size(); i++) { + this.addView(mPoints.get(i).getRandCdoeView()); + } + } + + } + + /** + * 添加标记 + */ + public void addPopup(int x, int y) { + SFLogN.info(TAG, "cricle num:" + mPoints.size()); + if (mPoints.size() < LIMT_NUM) { + addData(x, y); + } + } + + public void addData(int clickX, int clickY) { + SFLogN.info(TAG, "click x:" + clickX + "/y:" + clickY); + if (mPoints != null) { + addItem(clickX, clickY); + + if (mPoints.size() == LIMT_NUM && !isCheckCodeing) { + if (mFinishClickCallback != null) { + mFinishClickCallback.onFinishClick(getRondCode(), SFAuthType.AUTH_TYPE_RAND); + } + //记录当前正在校验code中 + isCheckCodeing = true; + } + } else { + SFLogN.warn2(TAG, "addData is fail", "points list is null"); + } + } + + private void addItem(int x, int y) { + //添加元素 + int num = mPoints.size() + 1; + ClickPoint point = new ClickPoint(x, y, num); + + LayoutParams params = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); + //第一次点击添加标签是 PictureTagView.Direction.Measure 让TagView自己测量方向 + RandCodeCircleView view = new RandCodeCircleView(getContext(), point); + view.setLayoutParams(new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, + LinearLayout.LayoutParams.WRAP_CONTENT)); + int w = MeasureSpec.makeMeasureSpec(0, + MeasureSpec.UNSPECIFIED); + int h = MeasureSpec.makeMeasureSpec(0, + MeasureSpec.UNSPECIFIED); + view.measure(w, h); + mChildHeight = view.getMeasuredHeight(); + mChildWidth = view.getMeasuredWidth(); + //标签在右面 点击位置 x-标签宽度 右边的标签并不是以圆点开始的 而是以左边的wei + params.leftMargin = x - mChildWidth / 2; + params.topMargin = y - mChildHeight / 2; + + //上下位置在视图内 + if (params.topMargin <= 0) { + params.topMargin = 0; + } else if ((params.topMargin + mChildHeight) > getHeight()) { + params.topMargin = getHeight() - mChildHeight; + } + if (params.leftMargin <= 0) { + params.leftMargin = 0; + } + if (params.rightMargin >= getWidth()) { + params.rightMargin = getWidth(); + } + + point.setRandCdoeView(view); + mPoints.add(point); + + this.addView(view, params); + } + + private boolean isClickableRect(int clickX, int clickY) { + if (mlayoutHeight <= 0 || mlayoutWith <= 0) { + return false; + } + + int clickHeight = (int) (mlayoutHeight - 32 * getResources().getDisplayMetrics().density); + Rect rect = new Rect(0, 0, mlayoutWith, clickHeight); + return rect.contains(clickX, clickY); + } + + private boolean hasView(int x, int y) { + //循环获取子view,判断xy是否在子view上,即判断是否按住了子view + for (int index = 0; index < this.getChildCount(); index++) { + View view = this.getChildAt(index); + int left = (int) view.getX(); + int top = (int) view.getY(); + int right = view.getRight(); + int bottom = view.getBottom(); + Rect rect = new Rect(left, top, right, bottom); + boolean contains = rect.contains(x, y); + //如果是与子view重叠则返回真,表示已经有了view不需要添加新view了 + if (contains) { + mTouchView = (RandCodeCircleView) view; + return true; + } + + } + + return false; + } + + /** + * 获取座标转换为string + * 拼接的json格式:{"coordinates":[[125,196],[255,93],[555,246]],"width":816,"height":576} + * + * @return + */ + private String getRondCode() { + for (int i = 0 ; i < mPoints.size(); i++) { + ClickPoint point = mPoints.get(i); + if (i == 0) { + user_position = "[" + point.getClickX() + "," + point.getClickY() + "]"; + } else { + user_position = user_position + ",[" + point.getClickX() + "," + point.getClickY() + "]"; + } + } + + String json = "{\"coordinates\"" + ":" + "[" + user_position + "]," + "\"width\":" + mlayoutWith + ",\"height\":" + mlayoutHeight + "}"; + return json; + } + + /** + * 清楚数据 + */ + public void clearView() { + removeAllViews(); + if (mPoints != null && mPoints.size() > 0) { + mPoints.clear(); + } + } + + /** + * 设置状态 + * @param checkCodeing + */ + public void setCheckCodeing(boolean checkCodeing) { + isCheckCodeing = checkCodeing; + } + + /** + * 获取当前是否正在校验图形验证码 + * @return + */ + public boolean getCheckCodeing() { + return isCheckCodeing; + } + + + /** + * 点击完成的的回调接口 + */ + public interface OnFinishClickListener { + void onFinishClick(String authJson, SFAuthType authType); + } + + /** + * 保存点击的数据 + */ + class ClickPoint { + private int clickX; + private int clickY; + private int num; + private RandCodeCircleView mRandCodeview; + + public ClickPoint(int x, int y, int num) { + this.clickX = x; + this.clickY = y; + this.num = num; + } + + public int getClickX() { + return clickX; + } + + public void setClickX(int clickX) { + this.clickX = clickX; + } + + public int getClickY() { + return clickY; + } + + public void setClickY(int clickY) { + this.clickY = clickY; + } + + public int getNum() { + return num; + } + + public void setNum(int num) { + this.num = num; + mRandCodeview.setPoint(this); + } + + public void setRandCdoeView(RandCodeCircleView view) { + this.mRandCodeview = view; + } + + public RandCodeCircleView getRandCdoeView() { + return mRandCodeview; + } + } +} diff --git a/app/src/main/java/com/rehome/zhdcoa/weiget/SFEditText.java b/app/src/main/java/com/rehome/zhdcoa/weiget/SFEditText.java new file mode 100755 index 0000000..bab49e7 --- /dev/null +++ b/app/src/main/java/com/rehome/zhdcoa/weiget/SFEditText.java @@ -0,0 +1,149 @@ +package com.rehome.zhdcoa.weiget; + +import android.content.Context; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import androidx.appcompat.widget.AppCompatEditText; +import android.text.Editable; +import android.text.TextWatcher; +import android.util.AttributeSet; +import android.view.MotionEvent; + +public class SFEditText extends AppCompatEditText implements TextWatcher { + + private Drawable leftDrawable = null; + private Drawable rightDrawable = null; + + // 控件是否有焦点 + private boolean hasFocus; + + private IMyRightDrawableClick mRightDrawableClick; + + public SFEditText(Context context) { + super(context); + } + + public SFEditText(Context context, AttributeSet attrs) { + super(context, attrs); + init(); + } + + public SFEditText(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, android.R.attr.editTextStyle); + init(); + } + + + private void init() { + //获取RadioButton的图片集合 + Drawable[] drawables = getCompoundDrawables(); + + leftDrawable = drawables[0]; + rightDrawable = drawables[2]; + + setCompoundDrawablesWithIntrinsicBounds(leftDrawable, null, null, null); + + //设置输入框里面内容发生改变的监听 + addTextChangedListener(this); + } + + + //设置显示图片的大小 + public void setCompoundDrawablesWithIntrinsicBounds(Drawable left, Drawable top, Drawable right, Drawable bottom) { + super.setCompoundDrawablesWithIntrinsicBounds(left, top, right, bottom); + + //这里只要改后面两个参数就好了,一个宽一个是高,如果想知道为什么可以查找源码 + if (left != null) { + left.setBounds(0, 0, 50, 50); + } + if (right != null) { + right.setBounds(0, 0, 50, 50); + } + if (top != null) { + top.setBounds(0, 0, 100, 100); + } + if (bottom != null) { + bottom.setBounds(0, 0, 100, 100); + } + setCompoundDrawables(left, top, right, bottom); + } + + + //光标选中时判断 + @Override + protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) { + super.onFocusChanged(focused, direction, previouslyFocusedRect); + this.hasFocus = focused; + if (focused) + setImageVisible(getText().length() > 0); + else + setImageVisible(false); + + } + + //设置清除图标的显示与隐藏,调用setCompoundDrawables为EditText绘制上去 + protected void setImageVisible(boolean flag) { + if (getCompoundDrawables()[2] != null) { + rightDrawable = getCompoundDrawables()[2]; + } + if (flag) { + setCompoundDrawables(getCompoundDrawables()[0], null, rightDrawable, null); + } else { + setCompoundDrawables(getCompoundDrawables()[0], null, null, null); + } + } + + //文本框监听事件 + @Override + public void onTextChanged(CharSequence text, int start, int lengthBefore, int lengthAfter) { + + if (hasFocus) { + if (text.length() > 0) + setImageVisible(true); + else + setImageVisible(false); + + } + } + + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + + } + + public void afterTextChanged(Editable s) { + + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + if (event.getAction() == MotionEvent.ACTION_UP) { + if (getCompoundDrawables()[2] != null) { + + boolean touchable = event.getX() > (getWidth() - getTotalPaddingRight()) + && (event.getX() < ((getWidth() - getPaddingRight()))); + if (touchable) { + if (mRightDrawableClick != null) { + //调用点击事件 + mRightDrawableClick.rightDrawableClick(); + } + } + } + } + + return super.onTouchEvent(event); + } + + public void setDrawableClick(IMyRightDrawableClick mMightDrawableClick) { + this.mRightDrawableClick = mMightDrawableClick; + } + + public void setRightDrawable(Drawable drawable) { + rightDrawable = drawable; + setCompoundDrawablesWithIntrinsicBounds(leftDrawable, null, rightDrawable, null); + } + + //自定义接口(实现右边图片点击事件) + public interface IMyRightDrawableClick { + void rightDrawableClick(); + } +} diff --git a/app/src/main/java/com/rehome/zhdcoa/weiget/SangforTextView.java b/app/src/main/java/com/rehome/zhdcoa/weiget/SangforTextView.java new file mode 100644 index 0000000..cd2ff03 --- /dev/null +++ b/app/src/main/java/com/rehome/zhdcoa/weiget/SangforTextView.java @@ -0,0 +1,93 @@ +package com.rehome.zhdcoa.weiget; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Rect; +import android.util.AttributeSet; +import android.view.View; + +import androidx.annotation.NonNull; +import androidx.appcompat.widget.AppCompatTextView; + +import com.rehome.zhdcoa.R; +import com.rehome.zhdcoa.utils.DpPxSpTransformUtil; + +/** + * @ProjectName: SDKDemo + * @Package: com.sangfor.sdkdemo.utils + * @ClassName: SangforTextView + * @Description: java类作用描述 + * @Author: yanghua + * @CreateDate: 2022/9/21 下午5:40 + */ +public class SangforTextView extends View { + + String mText; + + public void setText(String text) { + mText = text; + invalidate(); + } + + //构造方法 + public SangforTextView(@NonNull Context context) { + super(context); + } + + public SangforTextView(@NonNull Context context, AttributeSet attrs) { + super(context, attrs); + } + + public SangforTextView(@NonNull Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + @Override + protected void onDraw(Canvas canvas) { + //super.onDraw(canvas); + //画水平中心线和垂直中心线 + //drawCenterLineX(canvas); + //drawCenterLineY(canvas); + + //画底层黑色 + drawTextCenter(canvas); + + } + + //将文字写到中央 + private void drawTextCenter(Canvas canvas){ + //创建画笔 + Paint paint = new Paint(); + paint.setTextSize(DpPxSpTransformUtil.sp2px(12)); + paint.setColor(getResources().getColor(R.color.white)); + //让他垂直居中 + Paint.FontMetrics fontMetrics = paint.getFontMetrics(); + float baseLine = getHeight()/2 -(fontMetrics.descent + fontMetrics.ascent)/2; + //让他水平居中 + float x = getWidth()/2 - paint.measureText(mText)/2; + + canvas.drawText(mText,x,baseLine,paint); + } + + //画x轴的中心线 + private void drawCenterLineX(final Canvas canvas){ + Paint paint = new Paint(); + paint.setStyle(Paint.Style.FILL); + paint.setColor(Color.RED); + paint.setStrokeWidth(3); + + canvas.drawLine(getWidth()/2,0,getWidth()/2,getHeight(),paint); + } + + //画y轴的中心线 + private void drawCenterLineY(final Canvas canvas){ + Paint paint = new Paint(); + paint.setStyle(Paint.Style.FILL); + paint.setColor(Color.RED); + paint.setStrokeWidth(3); + + canvas.drawLine(0,getHeight()/2,getWidth(),getHeight()/2,paint); + } +} diff --git a/app/src/main/res/drawable/auth_button_login.xml b/app/src/main/res/drawable/auth_button_login.xml new file mode 100755 index 0000000..226a114 --- /dev/null +++ b/app/src/main/res/drawable/auth_button_login.xml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/auth_tab_header_text.xml b/app/src/main/res/drawable/auth_tab_header_text.xml new file mode 100755 index 0000000..54692d7 --- /dev/null +++ b/app/src/main/res/drawable/auth_tab_header_text.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/check_fail.png b/app/src/main/res/drawable/check_fail.png new file mode 100644 index 0000000..4c328b3 Binary files /dev/null and b/app/src/main/res/drawable/check_fail.png differ diff --git a/app/src/main/res/drawable/edit_bg.xml b/app/src/main/res/drawable/edit_bg.xml new file mode 100755 index 0000000..b494d9f --- /dev/null +++ b/app/src/main/res/drawable/edit_bg.xml @@ -0,0 +1,18 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/fresh_button.png b/app/src/main/res/drawable/fresh_button.png new file mode 100644 index 0000000..9299eb6 Binary files /dev/null and b/app/src/main/res/drawable/fresh_button.png differ diff --git a/app/src/main/res/drawable/randcode_back.png b/app/src/main/res/drawable/randcode_back.png new file mode 100644 index 0000000..ca447b8 Binary files /dev/null and b/app/src/main/res/drawable/randcode_back.png differ diff --git a/app/src/main/res/drawable/shape_check_fail.xml b/app/src/main/res/drawable/shape_check_fail.xml new file mode 100644 index 0000000..e06abc0 --- /dev/null +++ b/app/src/main/res/drawable/shape_check_fail.xml @@ -0,0 +1,13 @@ + + + + + + + + diff --git a/app/src/main/res/drawable/shape_check_fl.xml b/app/src/main/res/drawable/shape_check_fl.xml new file mode 100644 index 0000000..fd01dbd --- /dev/null +++ b/app/src/main/res/drawable/shape_check_fl.xml @@ -0,0 +1,11 @@ + + + + + + + + diff --git a/app/src/main/res/drawable/shape_circle_select.xml b/app/src/main/res/drawable/shape_circle_select.xml new file mode 100644 index 0000000..c613d91 --- /dev/null +++ b/app/src/main/res/drawable/shape_circle_select.xml @@ -0,0 +1,9 @@ + + + + + diff --git a/app/src/main/res/drawable/shape_corner.xml b/app/src/main/res/drawable/shape_corner.xml new file mode 100755 index 0000000..a9575a0 --- /dev/null +++ b/app/src/main/res/drawable/shape_corner.xml @@ -0,0 +1,10 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_auth_success.xml b/app/src/main/res/layout/activity_auth_success.xml new file mode 100644 index 0000000..85d761a --- /dev/null +++ b/app/src/main/res/layout/activity_auth_success.xml @@ -0,0 +1,193 @@ + + + + + + + +