顯示具有 Android 標籤的文章。 顯示所有文章
顯示具有 Android 標籤的文章。 顯示所有文章

2019年6月27日 星期四

「Android] 重覆利用現有的Activity

有多個Activity時,若想避免重覆產生Activity,可以在Intent中加入以下這個Flag

 FLAG_ACTIVITY_REORDER_TO_FRONT
例如

Intent intent=new Intent();
intent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
intent.setClass(nowAtvity.this,targetActivity.class);
startActivity(intent);


[Android]點擊畫面空白處Seekbar得到Focus問題

最近遇上一個UI的BUG,當點擊畫面空白處,

所有的Seekbar同時出現取得焦點的情況。

幾經測試發現是最外層的Constraintlayout有OnClick事件造成,

將OnClick由OnTouch替代即可解除這個問題。

2019年4月19日 星期五

[Android] TextView 中顯示不同字體大小

效果如下


這時就要透過Spannable 來做對應的處理

TextView textView=(TextView)findViewById(R.id.txt);

String str="您今天已經寫了100行書法";
//找到數字1的位置
int startIndex = str.indexOf('1');
//數字只有3個所以是startIndex+2
int endIndex = startIndex+2;
//初始化Spannable
Spannable textSpan = new SpannableStringBuilder(str);
//將數字1之前的字串,字體大小設為28 像素
textSpan.setSpan(new AbsoluteSizeSpan(28), 0, startIndex, Spannable.SPAN_INCLUSIVE_INCLUSIVE);
//將數字100字體大小設為45 像素
textSpan.setSpan(new AbsoluteSizeSpan(45), startIndex, endIndex+1 , Spannable.SPAN_INCLUSIVE_INCLUSIVE);
//將100之後的內容字體大小設為28像素
textSpan.setSpan(new AbsoluteSizeSpan(28), endIndex+1 , str.length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE);
//將Spannable設定給TextView
textView.setText(textSpan);

若要使用dip做為單位,則可以改成如下


TextView textView=(TextView)findViewById(R.id.txt);

String str="您今天已經寫了100行書法";
//找到數字1的位置
int startIndex = str.indexOf('1');
//數字只有3個所以是startIndex+2
 int endIndex = startIndex+2;
//初始化Spannable
Spannable textSpan = new SpannableStringBuilder(str);
//將數字1之前的字串,字體大小設為28
textSpan.setSpan(new AbsoluteSizeSpan(28,true), 0, startIndex, Spannable.SPAN_INCLUSIVE_INCLUSIVE);
//將數字100字體大小設為45
textSpan.setSpan(new AbsoluteSizeSpan(45,true), startIndex, endIndex+1 , Spannable.SPAN_INCLUSIVE_INCLUSIVE);
//將100之後的內容字體大小設為28
textSpan.setSpan(new AbsoluteSizeSpan(28,true), endIndex+1 , str.length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE);
//將Spannable設定給TextView
textView.setText(textSpan);



以下有針對幾個範圍的設定值做出解釋,但試起來還是有點怪怪的,多包函。

Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
不包含StartIndex及EndIndex
Spans of type SPAN_EXCLUSIVE_EXCLUSIVE do not expand to include text inserted at either their starting or ending point.

Spanned.SPAN_EXCLUSIVE_INCLUSIVE
不包含startIndex , 但包含endIndex
Non-0-length spans of type SPAN_EXCLUSIVE_INCLUSIVE expand to include text inserted at their ending point but not at their starting point.

Spanned.SPAN_INCLUSIVE_EXCLUSIVE
包含startIndex 但不包含endIndex
Non-0-length spans of type SPAN_INCLUSIVE_EXCLUSIVE expand to include text inserted at their starting point but not at their ending point.

Spanned.SPAN_INCLUSIVE_INCLUSIVE
包含startIndex及endIndex
Spans of type SPAN_INCLUSIVE_INCLUSIVE expand to include text inserted at either their starting or ending point.





2018年12月17日 星期一

[Android] 時間會倒流 ...Orz

最近遇到QC單位跑來說在把手機時間往前調幾小時後,APP失效了,調整前是OK的。

發生問題的程式碼大致上如下

long lastTime=0 ;
long now=0;

public void myFun()
{
    now=System.currentTimeMillis();

    if (now-lastTime>500)
   {
       //do something
      lastTime=now;
   }
}

當QC單位的人操作過一次後last=now 之後,System.currentTimeMillis() 會取出相對於1970/1/1 以來的亳秒,但此時將時間往前幾個小時之後,在這幾個小時內now將持續小於lastTime,因此不會有任何反應。

解決方式是使用絕對值來計算二者時間。


long lastTime=0 ;
long now=0;

public void myFun()
{
    now=System.currentTimeMillis();

    if (Math.abs(now-lasttime)>500)
   {
       //do something
      last=now;
   }
}

時間不會倒流....但手機可以。

2018年2月26日 星期一

[Android] TextView 的 freezesText

今天在處理當螢幕旋轉問題時,意外看到freezesText這個設定。

先來看一下Android的文件上怎麼說明的

android:freezesText
If set, the text view will include its current complete text inside of its frozen icicle in addition to meta-data such as the current cursor position. By default this is disabled; it can be useful when the contents of a text view is not stored in a persistent place such as a content provider. For EditText it is always enabled, regardless of the value of the attribute.
May be a boolean value, such as "true" or "false".

預設這個設定是關閉的,當你開啟時,它會完整保留TextView上的內容,遊標位置除外。

在EditText中,這個設定總是開啟的。

預設情況下,當我們用在執行過程中,透過程式去設定TextView內容後,當螢幕進行旋轉後,

TextView的內容會消失.


以下是範例程式碼,當按下Add Button後,會把TextView的內容+1
   @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        final TextView textView=(TextView)findViewById(R.id.txt);

        Button button=(Button)findViewById(R.id.add);

        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                int num=0;
                if (!textView.getText().toString().equals(""))
                {
                    num=Integer.parseInt(textView.getText().toString());
                }



                num++;

                textView.setText(String.valueOf(num));

            }
        });
    }

例如我按了六下Button,目前TextView的內容是6


當我進行螢幕旋轉後,TextView的內容不見囉



如果我將freezesText設為True


<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="com.example.boywhychen.context_menu.MainActivity">


    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/txt"
        android:freezesText="true"
        />

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/add"
        android:text="Add"
        android:layout_below="@+id/txt"
        />
</RelativeLayout>

執行結果如下


它會保留TextView的內容。

參考資料:

https://developer.android.com/reference/android/widget/TextView.html

2018年2月25日 星期日

[Android] String Format 中的 $

一般來說如果我們要透過String.Format 作字串格式化顯示 , 例如我們要顯示

1 + 10 = 11




我們會這樣用
String formatStr="%d + %d = %d";
TextView textView=(TextView)findViewById(R.id.txt);
String text=String.format(formatStr,1,10,11); textView.setText(text);

但也可以透過$指定索引順序,效果相同

String formatStr="%1$d + %3$d = %2$d";
TextView textView=(TextView)findViewById(R.id.txt);
String text=String.format(formatStr,1,11,10);
textView.setText(text);

2018年2月24日 星期六

[Android] FusedLocationProviderClient

FusedLocationProviderClient

The 11.0.0 release of the Google Play services SDK includes a new way to accessLocationServices. The new APIs do not require your app to manually manage a connection to Google Play services through a GoogleApiClient. This reduces boilerplate and common pitfalls in your app.


  • The API calls automatically wait for the service connection to be established, which removes the need to wait for onConnected before making requests.
  • It uses the Task API which makes it easier to compose asynchronous operations.
  • The code is self-contained and could easily be moved into a shared utility class or similar.
  • You don't need to understand the underlying connection process to start coding.

它是新的存取位置的API,整合在Google Play Service中,不再因為API不同而在過去使用LocationManager上有所差異。

2018年2月17日 星期六

[Android] Context Menu

最近看到一個蠻特別的東西--Context Menu--,沒想到可以這樣用,
幫Button加入"長按"選單




首先這是MainActivity的layout,XML的部份,只有一個Button

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="com.example.boywhychen.context_menu.MainActivity">

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/menu_btn"
        android:text="Context Menu" />
</RelativeLayout>

接下來我們需要在menu資料來下建立一個my_menu.xml


內容就是列出Menu中要顯示的Item內容以及順序

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">
<item android:id="@+id/color_blue"
    android:title="Blue"
    android:orderInCategory="1"
    app:showAsAction="ifRoom"/>
<item android:id="@+id/color_red"
    android:title="Red"
    android:orderInCategory="2"
    app:showAsAction="ifRoom"/>
<item android:id="@+id/color_green"
    android:title="Green"
    android:orderInCategory="3"
    app:showAsAction="ifRoom"/>
</menu>

接下來是主程式

package com.example.boywhychen.context_menu;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.ContextMenu;
import android.view.MenuItem;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;

public class MainActivity extends AppCompatActivity {



    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        //初始化Button
        Button menu_btn=(Button)findViewById(R.id.menu_btn);
        //幫Button註冊ContextMenu
        registerForContextMenu(menu_btn);
    }


    @Override
     public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
        super.onCreateContextMenu(menu, v, menuInfo);
        //要使用的Context Menu XML檔案
        getMenuInflater().inflate(R.menu.my_menu,menu);
    }

    @Override
    public boolean onContextItemSelected(MenuItem item) {
        //按下Context Menu Item要做的事
        switch ((item.getItemId()))
        {
            case R.id.color_blue:
                Toast.makeText(MainActivity.this,"Blue",Toast.LENGTH_SHORT).show();
                break;
            case R.id.color_green:
                Toast.makeText(MainActivity.this,"Green",Toast.LENGTH_SHORT).show();
                break;
            case R.id.color_red:
                Toast.makeText(MainActivity.this,"Red",Toast.LENGTH_SHORT).show();
                break;
        }

        return super.onContextItemSelected(item);
    }
}

執行效果(記住是長按才會跳出選單~我一開始都用click都沒反應~搞了半天)

點下Red後,Toast會顯示你按下的是Red




參考來源:

The Complete Android Material Design Course: Become a Pro
https://www.udemy.com/android-material-design-course-tutorial/learn/v4/overview
上課心得筆記一下

2018年1月9日 星期二

[Android] 正確關閉Socket

過去總是以為執行到了Socket.Close()時,Socket就結束了,沒有特別去注意,

直到遇到連接的裝置是Wi-Fi Module時,有TCP連線數限制時,發現卡死了,

唯有重開手機才有用,研究了很久,發現是以下幾個原因造成的。

1.因為動作需要頻繁重新建立Socket。

2.Android底層並不會在Socket.Close()時就立即關閉Socket,會等到資料傳輸完畢。


若要強制Socket立即關閉,在關閉Socket前先shutdown輸出入流。


socket.shutdownInput();  
socket.shutdownOutput();  

注意,這二行必須在inputstream.close()及outputstream.close()之前。




參考資料:

如何正确关掉socket?直接socket.close( )是否有风险?

2017年9月16日 星期六

「Android] Android 6.0 Wi-Fi and Networking Change

This release introduces the following behavior changes to the Wi-Fi and networking APIs.
  • Your apps can now change the state of WifiConfiguration objects only if you created these objects. You are not permitted to modify or deleteWifiConfiguration objects created by the user or by other apps.
  • Previously, if an app forced the device to connect to a specific Wi-Fi network by using enableNetwork() with the disableAllOthers=true setting, the device disconnected from other networks such as cellular data. In This release, the device no longer disconnects from such other networks. If your app’s targetSdkVersion is “20” or lower, it is pinned to the selected Wi-Fi network. If your app’s targetSdkVersion is “21” or higher, use the multinetwork APIs (such as openConnection()bindSocket(), and the new bindProcessToNetwork() method) to ensure that its network traffic is sent on the selected network.

以上是Android 6.0 Wi-Fi及網路進行的變更,中文的意思如下(若有翻錯請告知,謝謝):

  • 在過去APP可以進行修改手機中已連接過的Wi-FI設定值,也就是WifiConfiguration ,可以刪除/修改,但是在Android 6.0開始,您只能夠刪除/修改由APP本身建立的WifiConfiguration ,意思是說如果我想要把修改手機內某一個SSID的密碼,並自動連上它,是不會有效的,除非它是由您的APP新增而來的,若是手機中由使用者去手動輸入密碼的,或是其他APP建立的,都不行。
  • 過去,當您的手機連上Wi-Fi後,4G連線會被強制中斷,但在這次的6.0釋出中,裝置將不會與4G保持長時間的中斷,當您的APP targetSdkVersion <21時,它將會保持在您所選的Wi-Fi中,但targetSdkVersion >=21時,需要使用multinetwork APIs 來保證您所要傳輸的資料,會由您所選擇的網路(Wi-Fi / 4G)傳輸出去,例如openConnection()bindSocket()或是bindProcessToNetwork() 方法。






2017年7月3日 星期一

[Android] 省電模式偵測

這個方法不適合用在API 21 以下,那時沒有統一的省電模式API,各廠家大顯神通。

public boolean isPowerSaveMode()
    {

        PowerManager powerManager = (PowerManager)
        mContext.getSystemService(Context.POWER_SERVICE);
        if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP
                && powerManager.isPowerSaveMode())
        {
            return true;
        }
        else
        {
            return false;
        }
    }


資料來源:
How to check if Android Lollipop battery saver is onhttps://stackoverflow.com/questions/31896826/how-to-check-if-android-lollipop-battery-saver-is-on

2017年6月7日 星期三

「Android」Samsung S8 省電模式Ping指令異常

最近客戶反應S8不能正常使用我們家的APP,測了一整天都找不出問題,

最後把省電模式開啟動,出現問題。

最後確認是,當S8開啟省電模式時,PING 會沒有正確回應,導致程式判斷

網路是否順暢時出現錯誤回應。


但是若是使用HTTP連線一個IP位址,則是正常的。

EX: Ping 8.8.8.8   在省電模式開啟時,將得不到正常的回應(網頁可正常開啟)。
































而省電模式關閉後,Ping 8.8.8.8 則可以得到GOOGLE DNS如下回應。

64 bytes from 8.8.8.8: icmp_seq=0 ttl=43 time=50.361 ms
64 bytes from 8.8.8.8: icmp_seq=1 ttl=43 time=18.438 ms
64 bytes from 8.8.8.8: icmp_seq=2 ttl=43 time=19.047 ms
64 bytes from 8.8.8.8: icmp_seq=3 ttl=43 time=51.853 ms
64 bytes from 8.8.8.8: icmp_seq=4 ttl=43 time=18.523 ms
64 bytes from 8.8.8.8: icmp_seq=5 ttl=43 time=17.286 ms

64 bytes from 8.8.8.8: icmp_seq=6 ttl=43 time=17.853 ms

2017年2月24日 星期五

[Android] EditText讀取Bar Code Scanner的內容

程式碼如下


 @Override
    public boolean onKeyDown(int keyCode, KeyEvent event) {
        //only catch need char (different with every one) 
        char unicodeChar = (char) event.getUnicodeChar();
        if (((keyCode >= KeyEvent.KEYCODE_0 && keyCode <= KeyEvent.KEYCODE_Z) || unicodeChar == '-') )
        {
            isInput = true;
            unicodeChar = (char) event.getUnicodeChar();
            sb.append(unicodeChar);
            //wait a delay time 500ms for read all data ( adjustment for your bar code scanner)
            if (isInput) {
                mHandler.postDelayed(new Runnable() {
                    @Override
                    public void run() {
                        if (sb.length() != 0) {
                            String str = sb.toString();
                            //save your String to UI Component
                        isInput = false;
                    }
                }, DELAY);
            }
        }

        return false;
    }

我們透過OTG連接條碼掃描器,經過測試,有以下個注意事項。

(1)不是每台平板都支援OTG。

(2)不同平板對於條碼掃出來的結果可能會有差異。

2.1 有些平板,掃描出來的內容顯示到EditText極慢,像單手打一樣,難以判斷是輸入完成沒,或是出現異常的換行符號,掃描一組條碼可能出現多行換行。

2.2 OTG供電可能不穩定,導致條碼機異常。

(3)OTG接頭不要買線太細的,容易供電不足,小小一個轉換頭也不賴。

經驗分享,使用前請進行測試 。

(4)其實你可以以條碼的檢查碼進行確認是否己讀取完畢,不過此案用的這支條碼掃描器是可以自己識別多種格式條碼的,所以用這種檢查方式比較不適合 。

2017年2月22日 星期三

[Android] Webview 中點連結會開啟其他瀏覽器

最近使用WebView時遇到了一個問題,當我載入一個網頁後,

只要點點選連結,或是網頁裡的submit後就會要求開啟其他瀏覽器

(如果只有裝一種瀏覽器,就會直接由內建的瀏覽器開啟網頁,離開APP)。

我們先來看一下程式碼,載入YAHOO首頁(勿戰,因為它連結多,好示範)


import android.net.Uri;
import android.os.Bundle;
import android.support.design.widget.FloatingActionButton;
import android.support.design.widget.Snackbar;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.view.View;
import android.view.Menu;
import android.view.MenuItem;
import android.webkit.WebView;

import com.google.android.gms.appindexing.Action;
import com.google.android.gms.appindexing.AppIndex;
import com.google.android.gms.common.api.GoogleApiClient;

public class MainActivity extends AppCompatActivity {

    /**
     * ATTENTION: This was auto-generated to implement the App Indexing API.
     * See https://g.co/AppIndexing/AndroidStudio for more information.
     */
    private GoogleApiClient client;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);


        WebView webView = (WebView) findViewById(R.id.webview);
        webView.getSettings().setJavaScriptEnabled(true);
        webView.loadUrl("https://tw.yahoo.com");
    }
}

再來看看我說的情況,Yahoo是一個比較特別的網址,手機連上去會自己轉到手機版頁面,

所以會直接就跳出用瀏覽器開啟的要求。
































所以為了以防以上所提到的幾種問題,需要加入一段程式碼,設定WebView Client來處理

WebView中發生的其他事件。


import android.app.Activity;
import android.os.Bundle;
import android.webkit.WebView;
import android.webkit.WebViewClient;


public class MainActivity extends Activity {

    WebViewClient mWebViewClient;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);


        WebView webView = (WebView) findViewById(R.id.webview);
        webView.getSettings().setJavaScriptEnabled(true);
       
        mWebViewClient = new WebViewClient() {
            @Override
            public boolean shouldOverrideUrlLoading(WebView view, String url) {
                //process redirect URL
                view.loadUrl(url);
                return true;
            }
        };
        //set WebviewClient  to handle click hyper link , post...
        webView.setWebViewClient(mWebViewClient);
        webView.loadUrl("https://tw.yahoo.com");
    }


}

看看結果




點選連結之後

參考連結:


2016年8月1日 星期一

「Android」Mattermost Android 遇到 Self-signed certificate 解法


Mattermost Android版在遇到SSL使用Self-signed certificate  時, 會無法連接,

解決方法:

這裡下載原始碼開始Debug,一開始什麼都不改,先來看到底

登入時跳出的exception為


 javax.net.ssl.SSLHandshakeException: java.security.cert.CertPathValidatorException: Trust anchor for certification path not found.

看了CODE之後發現,由於Mattermost的HTTP 是透過Okhttp 這個套件,

所以不能輕易的叫他吞下去當做沒看到。

換個方式,既然certification path not found. 我們就指定給它。 

(1)首先我們要把Self-signed certificate放到專案中的raw資料夾











































接下來在MattermostService.java中新增一個setCertificates函式,

用來將我們放在raw資料夾中的憑證指定給 Okhttp Client

public void setCertificates(InputStream... certificates)
    {
        try
        {
            CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
            KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
            keyStore.load(null);
            int index = 0;
            for (InputStream certificate : certificates)
            {
                String certificateAlias = Integer.toString(index++);
                keyStore.setCertificateEntry(certificateAlias, certificateFactory.generateCertificate(certificate));

                try
                {
                    if (certificate != null)
                        certificate.close();
                } catch (IOException e)
                {
                }
            }

            SSLContext sslContext = SSLContext.getInstance("TLS");

            TrustManagerFactory trustManagerFactory =
                    TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());

            trustManagerFactory.init(keyStore);
            sslContext.init
                    (
                            null,
                            trustManagerFactory.getTrustManagers(),
                            new SecureRandom()
                    );
            client.setSslSocketFactory(sslContext.getSocketFactory());


        } catch (Exception e)
        {
            e.printStackTrace();
        }

    }

接下來修改MattermostService.java建構子,

新增一行呼叫setCertificates來讀取self cert


//add self cert
setCertificates(context.getResources().openRawResource(R.raw.nginx));

 public MattermostService(Context context) {
        this.context = context;

        String userAgent = context.getResources().getString(R.string.app_user_agent);

        cookieStore = new WebkitCookieManagerProxy();

        client.setCookieHandler(cookieStore);
        //add self cert
        setCertificates(context.getResources().openRawResource(R.raw.nginx));
        preferences = context.getSharedPreferences("App", Context.MODE_PRIVATE);
    }


到這裡, 你己經可以登入了, 但是會發現白白的一片, 因為還需要修改WebView SSL Error

的情況, 修改一下MainActivity.java

找到protected void setWebViewClient(WebView view)

增加一段處理SSL Error的程式碼


 @Override
public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
    // super.onReceivedSslError(view, handler, error);
    handler.proceed();
}


 protected void setWebViewClient(WebView view) {
        view.setWebViewClient(new WebViewClient() {

            @Override
            public void onPageFinished(WebView view, String url) {
                dialog.hide();
                Log.i("onPageFinished", "onPageFinished while loading");
                Log.i("onPageFinished", url);

                if (url.equals("about:blank")) {
                    AlertDialog.Builder alert = new AlertDialog.Builder(MainActivity.this);
                    alert.setTitle(R.string.error_retry);

                    alert.setPositiveButton(R.string.error_logout, new DialogInterface.OnClickListener() {
                        public void onClick(DialogInterface dialog, int whichButton) {
                            MainActivity.this.onLogout();
                        }
                    });

                    alert.setNegativeButton(R.string.error_refresh, new DialogInterface.OnClickListener() {
                        public void onClick(DialogInterface dialog, int whichButton) {
                            MainActivity.this.loadRootView();
                        }
                    });

                    alert.show();
                }
            }

            @Override
            public void onReceivedHttpError(WebView view, WebResourceRequest request, WebResourceResponse errorResponse) {
                Log.e("onReceivedHttpError", "onReceivedHttpError while loading");
                StringBuilder total = new StringBuilder();

                if (errorResponse.getData() != null) {
                    BufferedReader r = new BufferedReader(new InputStreamReader(errorResponse.getData()));
                    String line;
                    try {
                        while ((line = r.readLine()) != null) {
                            total.append(line);
                        }
                    } catch (IOException e) {
                        total.append("failed to read data");
                    }
                } else {
                    total.append("no data");
                }

                Log.e("onReceivedHttpError", total.toString());
            }

            @Override
            public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
               // super.onReceivedSslError(view, handler, error);
                handler.proceed();
            }

            @Override
            public void onReceivedError (WebView view, int errorCode, String description, String failingUrl) {
                Log.e("onReceivedErrord", "onReceivedError while loading (d)");
                Log.e("onReceivedErrord", errorCode + " " + description + " " + failingUrl);
                webView.loadUrl("about:blank");
            }

            @Override
            public boolean shouldOverrideUrlLoading(WebView view, String url) {
                Uri uri = Uri.parse(url);

                // Do not open in other browser if gitlab
                if (uri.getPath().contains("/oauth/authorize")) {
                    return false;
                }

                // Do not open in other browser if gitlab
                if (uri.getPath().contains("/oauth/token")) {
                    return false;
                }

                // Do not open in other browser if gitlab
                if (uri.getPath().contains("/api/v3/user")) {
                    return false;
                }

                // Do not open in other browser if gitlab
                if (uri.getPath().contains("/users/sign_in")) {
                    return false;
                }

                if (!uri.getHost().equalsIgnoreCase(appUri.getHost())) {
                    openUrl(uri);
                    return true;
                }

                if (uri.getPath().startsWith("/static/help")) {
                    openUrl(uri);
                    return true;
                }

                if (uri.getPath().contains("/files/get/")) {
                    openUrl(uri);
                    return true;
                }

                return super.shouldOverrideUrlLoading(view, url);
            }

            @Override
            public WebResourceResponse shouldInterceptRequest(WebView view, String url) {

                // Check to see if we need to attach the device Id
                if (url.toLowerCase().contains("/channels/")) {
                    if (!MattermostService.service.isAttached()) {
                        Log.i("MainActivity", "Attempting to attach device id");
                        MattermostService.service.init(MattermostService.service.getBaseUrl());
                        Promise<User> p = MattermostService.service.attachDevice();
                        if (p != null) {
                            p.then(new IResultListener<User>() {
                                @Override
                                public void onResult(Promise<User> promise) {
                                    if (promise.getError() != null) {
                                        Log.e("AttachDeviceId", promise.getError());
                                    } else {
                                        Log.i("AttachDeviceId", "Attached device_id to session");
                                        MattermostService.service.SetAttached();
                                    }
                                }
                            });
                        }
                    }
                }

                // Check if deviceID is missing
                if (url.toLowerCase().contains("/login")) {
                    MattermostService.service.SetAttached(false);
                }

                // Check to see if the user was trying to logout
                if (url.toLowerCase().endsWith("/logout")) {
                    MattermostApplication.handler.post(new Runnable() {
                        @Override
                        public void run() {
                            onLogout();
                        }
                    });
                }

                return super.shouldInterceptRequest(view, url);
            }
        });
    }

透過以上的修改後, Android版就可以正常登入Self-signed certificate 的Mattermost網頁