Blog

  • Kiến trúc phân tầng (Layered Architecture) (Phần 1)

    Kiến trúc phân tầng (Layered Architecture) (Phần 1)

    Kiến trúc phân tầng(hay còn được gọi là kiến trúc n-tier) là kiến trúc phổ biến nhất. Kiến trúc này được xem là chuẩn không chính thức cho các ứng dụng Java EE và được biết đến rộng rãi bởi hầu hết kiến trúc sư, nhà thiết kế và nhà phát triển. Kiến trúc này quen thuộc với các cơ cấu tổ chức và truyền thông CNTT truyền thống, được tìm thấy ở hầu hết các công ty khiến nó trở thành một lựa chọn tự nhiên cho các công ty phát triển ứng dụng doanh nghiệp.

    Giới thiệu kiến trúc phân tầng

    Các thành phần bên trong kiến trúc phân tầng được tổ chức thành các tầng nằm ngang, mỗi tầng thực hiện một vai trò cụ thể trong ứng dụng, ví dụ như tầng trình diễn(presentation) hay tầng nghiệp vụ (business). Mặc dù kiến trúc phân tầng không qui định số lượng hay các loại tầng phải tồn tại, hầu hết các kiến trúc phân tầng bao gồm 4 tầng: tầng trình diễn, tầng nghiệp vụ, tầng lưu trữ(persistence), tầng cơ sở dữ liệu(database).

    4-tier

    Trong một vài trường hợp tầng nghiệp vụ và tầng lưu trữ có thể được kết hợp thành một tầng nghiệp vụ, đặc biệt khi logic lưu trữ (SQL hay HSQL) được nhúng bên trong các thành phần của tầng nghiệp vụ. Vì vậy với các ứng dụng nhỏ có thể chỉ có ba tầng, ngược lại với các ứng dụng lớn hoặc các ứng dụng có nghiệp vụ phức tạp có thể chứa năm tầng hoặc nhiều hơn.

    Mỗi tầng trong kiến trúc phân tầng có một vai trò và trách nhiệm cụ thể trong ứng dụng. Ví dụ tầng trình diễn sẽ có trách nhiệm xử lí tất cả giao diện người dùng và logic giao tiếp trình duyệt, trái lại tầng nghiệp vụ sẽ chịu trách nhiệm thực thi các quy tắc nghiệp vụ cụ thể. Mỗi tầng trong kiến trúc phân tầng trừu tượng hoá các công việc cần phải hoàn thành để đáp ứng một yêu cầu nghiệp vụ cụ thể. Ví dụ tầng trình diễn không cần phải biết hay lo lắng về việc lấy dự liệu khách hàng như thế nào; nó chỉ cần hiển thị các thông tin đó lên màn hình theo định dạng cụ thể. Tương tự như vậy, tầng nghiệp vụ không cần bận tâm làm thế nào định dạng dữ liệu khách hàng để hiển khị lên màn hình hoặc ngay cả việc dự liệu khách hàng được lưu trữ ở đâu; nó chỉ cần lấy dữ liệu từ tầng lưu trữ, thực hiện logic nghiệp vụ dựa trên dữ liệu đó(tính toán các giá trị hay tổng hợp dữ liệu), và truyền các thông tin đó tới tầng trình diễn.

    Một trong những tính năng mạnh mẹ của kiến trúc phân tầng sự tách biệt các mỗi bận tâm giữa các thành phần. Các thành phần bên trong một tầng cụ thể chỉ giải quyết các logic liên quan đến tầng đó. Ví dụ các thành phần ở tầng trình diễn chỉ giải quyết các logic hiển thị, ngược lại các thành phần cư trú ở tầng nghiệp vụ chỉ giải quyết các logic nghiệp vụ. Việc phân loại các loại thành phần như thế này giúp cho việc xây dựng mô hình trách nhiệm và vai trò có hiệu quả trở nên dễ dàng hơn, đồng thời giúp bạn dễ dàng phát triển, kiểm thử, quản lí, và bảo trì các ứng dụng nhờ vào các giao diện thành phần được mô tả rõ ràng và phạm vi thành phần được giới hạn.

    Tầng đóng(closed layer)

    Chúng ta cùng xem hình dưới đây:

    closed_layer.png

    Mỗi tầng trong kiến trúc này được đánh dấu CLOSED. Đây là một khái niệm rất quan trọng trong kiến trúc phân tầng. Một tầng đóng có nghĩa là một yêu cầu(request) đi từ tầng này sang tầng khác phải đi qua tầng ngay bên dưới nó để đi tới tầng tiếp theo bên dưới tầng đó. Ví dụ một request bắt nguồn từ tầng trình diễn phải đi qua tầng trình diễn sau đó tới tầng lưu trữ trước cuối cùng chốt lại ở tầng cơ sở dữ liệu.

    Tại sao không cho phép tầng trình diễn truy cập trực tiếp đến tầng lưu trữ hay tầng cơ sở dữ liệu?

    Truy cập trực tiếp cơ sở dữ liệu từ tầng trình diễn thì nhanh hơn là thông qua một loạt các tầng không cần thiết chỉ để lấy ra hay lưu lại thông tin cơ sở dữ liệu. Câu trả lời cho câu hỏi này nằm ở một khái niệm tầng cô lập(layers of isolation).

    Tầng cô lập

    Khái niệm tầng cô lập có nghĩa là những thay đổi được tạo ra trong một tầng của kiến trúc nhìn chung không có tác động hay ảnh hướng đến các thành phần của tầng khác: thay đổi được cô lập trong các thành phần bên trong lớp đó và một lớp nào đó có có thể có liên quan(ví dụ như tầng lưu trữ có chứa SQL). Nếu chúng ta cho phép tầng trình diễn truy cập trực tiếp tới tầng lưu trữ thì những thay đổi được tạo ra với SQL trong tầng lưu trữ sẽ có tác động tới cả tầng nghiệp vụ và tầng trình diễn, do đó tạo ra một ứng dụng liên kết rất chặt chẽ với rất nhiều phụ thuộc chéo giữa các thành phần. Những ứng dụng như thế này quá cứng nhắc và tốn kém khi có thay đổi.

    Khái niệm tầng cô lập cũng có nghĩa là mỗi một tầng độc lập với các tầng khác, do đó chúng biết rất ít hoặc không biết về hoạt động bên trong của các tầng khác trong kiến trúc. Để hiểu được sức mạnh và tầm quan trọng của khái niệm này, chúng ta cùng xem xét nỗ lực tái cấu trúc để chuyển đổi presentation framework từ JSP (Java Server Pages) to JSF (Java Server Faces). Giả sử rằng hợp đồng(model) được sử dụng giữa tầng trình diễn và tầng nghiệp vụ không thay đổi, tầng nghiệp vụ không bị ảnh hưởng bởi việc tái cấu trúc và giữ độc lập hoàn toàn với các loại giao diện người dùng được sử dụng bởi tầng trình diễn.

    Tầng mở

    Tầng đóng tạo điều kiện thuận lợi cho tầng cô lập và do đó giúp cô lập thay đổi trong kiến trúc, đôi khi tầng mở có ý nghĩa đối với các tầng nhất định. Giả sử bạn muốn thêm vào tầng dịch vụ chia sẻ(shared services) chứa các thành phần dịch vụ dùng chung được truy cập bởi các thành phần trong tầng nghiệp vụ(các lớp tiện ích dữ liệu hay chuổi, các lớp kiểm tra và ghi nhật ký). Trong trường hợp này việc tạo ra tầng dịch vụ thường là ý tưởng tốt bởi lẽ về mặt kiến trúc thì nó hạn chế các truy cập vào các dịch vụ chia sẻ đổi với tầng nghiệp vụ(chứ không phải tầng trình diễn). Không có sự tách biệt về tầng, về mặt kiến trúc không có gì hạn chế tầng trình diễn truy cập vào các dịch vụ dùng chung này, gây khó khăn cho việc quản lý hạn chế truy cập.

    Trong ví dụ này, tầng dịch vụ mới sẽ có thể sẽ nằm bên dưới tầng nghiệp vụ để chỉ rằng các thành phần trong tầng dịch vụ này không thể truy cập từ tầng trình diễn. Tuy nhiên, điều này có nghĩa rằng tầng nghiệp vụ lúc này bắt buộc phải đi qua tầng dịch vụ để tới tầng lưu trữ, điều này không có ý nghĩa gì cả. Đây là vấn đề lâu đời của kiến trúc phân tầng. Để giải quyết vấn đề này chúng ta tạo ra tầng mở trong kiến trúc.

    open_layer.png

    Các tầng dịch vụ trong trường hợp này được đánh dấu là OPEN, có nghĩa là các yêu cầu được cho phép đi qua tầng mở và đi trực tiếp tới tầng bên dưới. Trong ví dụ trên vì tầng dịch vụ là tầng mở nên tầng nghiệp vụ bây giờ được phép bỏ qua nó và đi trực tiếp tới tầng lưu trữ, điều này hoàn toàn hợp lí.

    Chúng ta có thể tận dụng khái niệm về tầng mở và đóng để xác định mối quan hệ giữa các tầng trong kiến trúc và luồng yêu cầu cũng như cung cấp cho nhà thiết kế, nhà phát triển các thông tin cần thiết để hiểu các hạn chế truy cập tới các tầng khác nhau trong kiến trúc. Việc không thể xác định chính xác các tầng nào trong kiến trúc là mở hay đóng và tại sao thường dẫn tới các cấu trúc liên kết chặt chẽ và dễ vỡ, chúng rất khó để kiểm thử, bảo trì và triển khai.

    Tài liệu tham khảo

    • Software Architecture Patterns
  • Nghiên cứu

    Nghiên cứu

    Nghiên cứu

    Thi thoảng mình nhận được một số câu hỏi của bạn bè, trong đó có câu: Mày làm Nghiên cứu hay Ứng dụng ??

    Mình trộm nghĩ, nếu nói tao làm nghiên cứu thì chỉ đúng một phần mà nói tao làm ứng dụng thì cũng chỉ đúng được một phần. Nên tấu hài rằng: “Tao làm nghiên cứu ứng dụng”. Kể ra cũng… hợp lý ?

    Mình luôn tự coi bản thân là (hoặc cố gắng để trở thành) một Kỹ sư phần mềm, bấy lâu nay mình vẫn luôn nghĩ vậy. Và khi gặp một vấn đề kỹ thuật khó, thì một Kỹ sư bắt buộc (hoặc cần) phải có năng lực nghiên cứu giải pháp. Lấy ví dụ, mình đang cần phát triển và triển khai 1 mô hình Machine Learning (nhẹ, không cần GPU) và 1 mô hình Deep Learning (nặng, có thể cần GPU) trên môi trường AWS; yêu cầu Kỹ thuật đặt ra là tối thiểu việc vận hành hạ tầng cũng như việc Update mô hình khi cần thiết –> có rất nhiều solutions khác nhau –> cần Re-search. Một vài phân tích đơn giản như; do business mà các hệ thống này ít được sử dụng thường xuyên –> chúng ta nên dùng Serverless để tối ưu chi phí (hoặc Spot instance); với hệ thống Machine Learning, do đặc điểm nhẹ, suy diễn nhanh –> có thể sử dụng AWS Lambda –> đến đây cần nghiên cứu cách sử dụng Aws Lambda sao cho hiệu quả (bởi một số giới hạn Package size) –> cần thực nghiệm các giải pháp khác nhau: Lambda layers, Elastic File System, Docker Image,… Một ví dụ khác là thằng bạn mình loay hoay tìm cách Crawl hết sản phẩm của các trang thương mại điện tử, rồi tìm cách lưu trữ và sử dụng sao cho hiệu quả,… cũng phải mất khá nhiều thì giờ để Re-search (search đi search lại).

    Tóm lại, mình nghĩ đơn giản Nghiên cứu (hay Research) tức là “search đi search lại”. Không cứ phải phát minh ra một cái vĩ đại, chưa ai làm mới là Nghiên cứu, mà đơn giản chỉ cần giải quyết đúng bài toán mà mình gặp phải… đó cũng có thể gọi là nghiên cứu được chứ nhỉ ? Nghiên cứu thường đi kèm với năng lực phân tích vấn đề, bởi chỉ có phân tích thì chúng ta mới chỉ ra được ưu nhược điểm của giải pháp, tại sao nó lại phù hợp với bài toán của mình. Chính nhờ sự nghiên cứu mà năng lực, kỹ năng của Kỹ sư tăng lên –> qua đó sẽ có khả năng phán đoán, quyết định giải pháp tốt hơn, nhạy bén hơn –> Thu nhập cao hơn ?

    Technologies come and technologies go, but insight is forever.

  • [MyBatis] Sử dụng MyBatis với Spring Boot

    [MyBatis] Sử dụng MyBatis với Spring Boot

    MyBatis là gì?

    MyBatis là một framework nổi tiếng trong cộng đồng Java. Nó là triển khai của tầng lưu trữ trong kiến trúc phân tầng trên nền tảng Java tương tự như Hibernate hoặc ngay cả là JDBC thuần. Nó giúp việc triển khai tầng lưu trữ trở nên đơn giản hơn với nhà phát triển. Thông tin về MyBatis bạn có thể tham khảo tại đây. Nội dung bài viết này tập trung vào cách sử dụng MyBatis cùng với Spring Boot.

    MyBatis-Spring-Boot-Starter

    Để sử dụng MyBatis với Spring Boot chúng ta sử dụng thư viện MyBatis-Spring-Boot-Starter. Đây là thư viện hỗ trợ cấu hình nhanh chóng một ứng dụng Spring Boot có sử dụng MyBatis. Thư viện này được hỗ trợ chính thức từ nhóm phát triển MyBatis. Các bước cấu hình tại đây.

    Repository hay Mapper

    Mapper là một Java interface mà các phương thức được ánh xạ tới các truy vấn SQL tương ứng. Mặc định MyBatis sẽ ánh xạ các method được định nghĩa trong các interface được đánh dấu với annotation @Mapper tới các truy vấn SQL tương ứng.

    ProductRepository

    Dưới đây là một ví dụ về Mapper. ProductRepository được định nghĩa trong package app.demo.mybatis.repository

    
    @Mapper
    public interface ProductRepository {
    
      void create(Product product);
    }
    

    Để định nghĩa truy vấn SQL tương ứng chúng ta tạo ProductRepository.xml trong thư mục app.demo.mybatis.repository bên trong resources.

    
    
    
    
      
        
      
    
    

    Trong ví dụ trên chúng ta đang ánh xạ phương thức create với câu truy vấn INSERT. Khi phương thức create được gọi thì câu truy vấn sẽ được thực thi. Kết quả câu truy vấn sẽ được trả về phương thức create.

    DataSource

    Trong bài viết này chúng ta sẽ sử dụng MySQL để thực hành với MyBatis. Chúng ta có thể sử dụng docker để tạo container chạy MySQL với câu lệnh sau:

    docker run --name demo -e MYSQL_ROOT_PASSWORD=demo@123 -p 3306:3306 -d mysql --lower_case_table_names=1
    

    Với câu lệnh trên chúng ta đã tạo xong MySQL với schema demo cũng như mật khẩu cho tài khoản rootdemo@123.

    Để cấu hình data source với Spring Boot chúng ta thêm các cấu hình sau trong application.yml:

    spring:
      datasource:
        url: jdbc:mysql://localhost:3306/demo?createDatabaseIfNotExist=true
        username: root
        password: demo@123
        driverClassName: com.mysql.cj.jdbc.Driver
    

    BindingException

    org.apache.ibatis.binding.BindingException: Invalid bound statement (not found)
    

    Lỗi này xuất hiện khi chưa cấu hình thuộc tính mybatis.mapper-locations. Cấu hình thuộc tính này trong application.yml hoặc application.properties như sau:

    mybatis:
      mapper-locations: "classpath:app.demo.mybatis.repository/*.xml"
    
    mybatis.mapper-locations=classpath:app.demo.mybatis.repository/*.xml
    

    Tổng kết

    Trong bài viết này tôi đã hướng dẫn các bạn các bước cơ bản để tạo một ứng dụng Spring Boot có sử dụng MyBatis. Hi vọng bài viết sẽ giúp ích cho các bạn mới bắt đầu tiếp cận với MyBatis cũng như là cung cấp một hướng dẫn khi bắt đầu xây dựng một dự án.

  • Spring Security: Tìm hiểu về internal flow

    Spring Security: Tìm hiểu về internal flow

    Spring Security là gì?

    Spring Security là một framework được cung cấp bởi Spring cung cấp khả năng xác thực, bảo vệ, kiểm soát truy cập và có khả năng tuỳ biến cao. Tập trung chủ yếu vào Authentication và Authorization cho một ứng dụng Java.

    Giống như hầu hết các Spring projects khác, sức mạnh thực sự của Spring Security đến từ việc nó có thể dễ dàng mở rộng khi cần thiết với những yêu cầu cụ thể trong một dự án.

    Benefits/features chính:

    • Hỗ trợ authentication và authorization một cách toàn diện.
    • Ngăn chặn các nguy cơ bảo mật đến từ Cross-site Forgery, CSRF Attacks, ClickJacking,…
    • Hỗ trợ tích hợp với Spring Web MVC
    • Hỗ trợ tích hợp với Servlet API

    Internal Workflow: Cách Spring Security hoạt động?

    Dưới đây là workflow cách mà Spring Security mặc định (User Credentials) hoạt động:

    User Credentials Authentication workflow

    Ta hãy cùng đến với những objects chính có trong flow và tìm hiểu định nghĩa của chúng nhé.

    Spring Security Filters:

    Spring Security Authentication Filters là những filter sẽ nằm giữa client request với server. Khi nhận được request, các filter sẽ tách lọc những thông tin từ request thành các authentication details (username, password, roles,…). Default Spring Security sẽ sử dụng class UsernamePasswordAuthenticationFilter.

    UsernamePasswordAuthenticationFilter extends từ Abstract class AbstractAuthenticationProcessingFilter.

    Authentication: là một base object làm nhiệm vụ validate user credentials nhận được từ phía client. Ở behavior mặc định, Authentication object sẽ là class UsernamePasswordAuthenticationToken.

    UsernamePasswordAuthenticationToken sẽ được sử dụng để chứa user credentials.

    AuthenticationManager:

    AuthenticationManager là một interface với method authenticate() làm nhiệm vụ xác định những Authentication providers phù hợp nhất để xử lý Authentication object nhận được từ filters. AuthenticationManager sẽ nhận kết quả authenticate từ Provider (Success hoặc Not success). Nếu không success, nó sẽ thử một provider phù hợp khác.

    Ở behavior mặc định của Spring security, class ProviderManager sẽ được chọn để xử lý các request.

    ProviderManager implements interface AuthenticationManager.

    AuthenticationProvider:

    AuthenticationProvider là những classes implement interface AuthenticationProvider với method authenticate() làm nhiệm vụ xử lý các logic liên quan đến authentication. DaoAuthenticationProvider sẽ là authentication provider mặc định cho behavior mặc định của Spring Security.

    DaoAuthenticationProvider.

    UserDetailsService: là interface chứa thông tin, schema của user details. Ở behavior mặc định, Spring Security sẽ sử dụng class InMemoryUserDetailsManager, với method loadUserByUsername() để lấy ra thông tin của user từ memory của hệ thống.

    PasswordEncoder: là interface có nhiệm vụ encode, encrypt và decrypt password của user, validate và trả về kết quả valid/invalid cho Authentication Provider xử lý.

    Security Context:

    Sau khi Spring Security đã validate đủ, user details sẽ được lưu vào Security context. Ở lần truy cập tới, thông tin user sẽ được filter retrieve ở đây thay vì thực hiện đầy đủ các bước flow như ở trên.

    Kết luận

    Trên đây là tổng hợp về Spring Security cũng như một flow mặc định của Spring Security sẽ diễn ra như thế nào. Tất nhiên, còn rất nhiều những vấn đề to lớn khác từ Spring Security mà phạm vi bài viết không thể mô tả đủ. Hi vọng, qua bài viết trên, bạn đã có thể có cái nhìn tổng quan nhất về Spring Security, rất cảm ơn bạn đã dành thời gian ra để đọc qua bài viết trên của mình.

    Tài liệu tham khảo

    https://spring.io/projects/spring-security

    https://blog.knoldus.com/spring-security-internal/

    https://www.linkedin.com/pulse/how-does-spring-security-works-internally-ayush-jain/

  • Code sướng tay: Bước 1

    Có một vấn đề mà ai code cũng cần làm, đó là đặt tên (cho lớp, hàm, biến…).

    Ai cũng có thể viết code máy hiểu được, nhưng chỉ số ít viết code mà người hiểu được.

    “I’m not a great programmer, I’m just a good programmer with great habits.”

    Martin Fowler (tác giả sách Refactoring)

    Hồi còn đi học, chúng ta hay làm toán và đặt tên các biến là x,y,z,t… các hằng là a,b,c,…m,n…. Chúng ta hay giải lí với các biến là U, I, g, … Tương tự với các môn cần đến toán như hóa, sinh… Và tin học cũng là một môn dùng đến toán như vậy. Chúng ta tiếp tục lặp lại những gì mình từng biết:

    int a, b, c;

    float x, y, z;

    double aa, bb, xx, yy;

    Đặt những cái tên mà chúng ta thấy là đúng đắn, nhưng trông thật vô nghĩa. Trước mình cũng code rất nhiều những thuật toán, mà giờ đọc lại, không còn hiểu được ngày xưa mình code gì thế này, hàm này để làm gì, giải quyết bài toàn gì? Đừng đặt cho biến của bạn những cái tên quá ngắn như thế, vì nó chẳng thể hiện được tí ý nghĩa nào của biến đó hết.

    Đố ai hiểu code đang nghĩ gì :v

    Sau một chút thì mình học được cách thêm comment vào code để dễ hiểu hơn, như thế này:

    code cùng một chút comment (l=left, r=right)

    Tuy nhiên, sau này mấy anh khóa trên đi làm xong đến lớp bảo: “Ở công ty code giờ về lớp code nhìn cứ ngứa mắt. Đi làm code là tên biến nó cứ dài dài cơ. Càng dài càng tốt ae nhề, dài nhìn mới thuận mắt (nói với mấy anh cùng đi làm).” Lúc đấy mình cũng cười trừ thôi, vì thấy code của mình đọc dễ hiểu lắm rồi, đặt tên dài làm gì đâu. Hồi đó mình còn học các cách để rút gọn/ viết tắt tên biến như là lấy chữ cái đầu, bỏ đi nguyên âm, lấy 3 kí tự đầu… Mãi sau mới thấy, IDE gợi ý đến tận răng, tiếc gì mấy phím gõ mà phải học viết tắt quá mức như vậy nhỉ (thật ra học cái này cũng hữu ích khi mà đọc code người khác họ viết lst còn biết là list chả hạn .-.) Đừng cố gắng viết tắt tên biến, trừ khi là những từ viết tắt mà ai cũng biết và hiểu rõ.

    Hóa ra là, hàng tốt thì không cần quảng cáo, code tốt thì không cần comment, tự những cái tên trong code cần phải làm việc của nó khiến đoạn code dễ đọc, dễ hiểu (trừ những logic phức tạp và khó hiểu). Khi tên một hàm thể hiện nó làm gì, người ta sẽ không cần đọc đến đoạn code bên trong nó nữa. Trong cuộc đời 1 coder, thời gian đọc code luôn nhiều hơn thời gian viết ra nó (vì code thường sẽ cần được sửa), nên nếu đặt một cái tên tốt, sau này đọc lại bạn sẽ đỡ áp lực hơn khi đọc lại code của chính mình (và đỡ muốn đấm bản thân hơn, đỡ mất thời gian đọc comment, đỡ phải vò đầu bứt tai suy nghĩ ôi cái đoạn code này dùng để làm gì thế nhỉ).

    Nếu biến của bạn có đơn vị, hãy cố gắng thêm đơn vị vào tên của nó. Ví dụ thay vì hàm startAnimation(int delayTime) hãy đặt là startAnimation(int delayTimeInSeconds) hoặc nếu tốt hơn nữa, hãy thay kiểu của nó thành một kiểu mà sẽ khó bị nhầm lẫn, ví dụ như Duration trong Dart, bạn sẽ không cần quan tâm đến đơn vị của nó nữa. Khi code, nếu bạn phát hiện ra một cái tên khó hiểu hoặc dễ hiểu lầm và bạn có thể đặt cho nó cái tên tốt hơn, hãy đổi nó ngay khi bạn có thể (miễn là nó không ảnh hưởng tới đồng nghiệp của bạn, nếu có ảnh hưởng, bạn có thể hỏi ý kiến họ trước hoặc thêm một chút comment để sau này không tốn thời gian quá nhiều để hiểu nó nữa). Nhỏ thôi nhưng sẽ khiến bạn của sau này phải cảm ơn bạn đấy :3

    Một số cách đặt tên mà bạn nên tham khảo:

    • Nên dùng tiếng anh để đặt tên vì hầu hết các kí hiệu trong code đều là tiếng anh và tiếng anh không dễ bị nhầm lẫn như tiếng việt không dấu
    • Tuân thủ nguyên tắc đặt tên của ngôn ngữ/ dự án mà bạn làm
    • Đặt tên cho hàm bằng các động từ, cho các lớp bằng danh từ
    • Chỉ sử dụng biến bắt đầu bằng is/ are/ has… để đặt cho các biến/ thuộc tính/ hàm trả lời cho câu hỏi yes/no
    • Khi bạn không nghĩ ra được một cái tên cho biến/hàm/lớp bạn cần vì nó dễ bị nhầm lẫn với những biến/hàm/lớp khác, đây là lúc nên đặt lại tên cho cả những biến/hàm/lớp khác kia (chứ đừng thay bằng một từ đồng nghĩa, điều này sẽ khiến mọi người bối rối vì không biết chúng khác nhau ở đâu .-.)

    Tóm tắt các nguyên tắc:

    1. Tuân thủ convention đặt tên của ngôn ngữ/dự án đang làm
    2. Tên biến nên thể hiện ý nghĩa của nó, đừng cố gắng viết tắt nó
    3. Tham khảo một số nguyên tắc chung

    Lần đầu viết bài, mong là sẽ có lần sau hehe~

  • ANDROID BLUETOOTH CLASSIC – PART 4

    ANDROID BLUETOOTH CLASSIC – PART 4

    Chào các bạn, hôm nay mình tiếp tục chuỗi bài liên quan đến Bluetooth Classic trong Android. Bài hôm nay mình tập trung vào phần Transfer Bluetooth Data.

    Sau khi bạn đã kết nối thành công với thiết bị Bluetooth, mỗi thiết bị có một BluetoothSocket được kết nối. Sau đó bạn có thể chia sẻ thông tin giữa các thiết bị bằng cách thông qua đối tượng BluetoothSocket, về cơ bản các bước đọc ghi dữ, để truyền dữ liệu như sau:

    • Get InputStream và OutputStream xử lý truyền qua Socket bằng cách sử dụng getInputStream() và getOutputStream() tương ứng.
    • Đọc và ghi dữ liệu vào các luồng sử dụng read(byte[]) và write(byte[]).

    Mình sẽ tạo một ví dụ minh hoạ, với ứng dụng Chat giữa client và server bằng Bluetooth Classic.

    Đầu tiên chúng ta tạo một Object chứa phương thức đọc, ghi. Xử lý Read, Write thông qua 2 object InputStream và OutputStream.

    private val bluetoothAdapter: BluetoothAdapter = adapter
    private val handler: Handler = mHandler
    
    private lateinit var bluetoothSending: BluetoothSending
    
    inner class BluetoothSending(bluetoothSocket: BluetoothSocket?) : Thread() {
    
        private val inputStream: InputStream?
        private val outputStream: OutputStream?
    
        init {
            var tempIn: InputStream? = null
            var tempOut: OutputStream? = null
            try {
                tempIn = bluetoothSocket?.inputStream
                tempOut = bluetoothSocket?.outputStream
            } catch (e: IOException) {
                e.printStackTrace()
            }
            inputStream = tempIn
            outputStream = tempOut
        }
    
        override fun run() {
            val buffer = ByteArray(1024)
            var bytes: Int
    
            // Keep listening to the InputStream until an exception occurs.
            while (true) {
                try {
                    bytes = inputStream?.read(buffer)!!
                    handler.obtainMessage(Contstants.STATE_MESSAGE_RECEIVED, bytes, -1, buffer)
                        .sendToTarget()
                } catch (e: IOException) {
                    Log.e("PhongPN", "Bluetooth Reading Data Error", e)
                    e.printStackTrace()
                }
            }
        }
    
        fun write(bytes: ByteArray?) {
            try {
                outputStream?.write(bytes)
            } catch (e: IOException) {
                Log.e("PhongPN", "Bluetooth Writing Data Error", e)
                e.printStackTrace()
            }
        }
    }
    

    Tiếp đến chúng ta tạo một Server Socket. Lắng nghe các kết nối đến nó.

    inner class ServerClass : Thread() {
    
        private var serverSocket: BluetoothServerSocket? = null
    
        init {
            try {
                serverSocket =
                    bluetoothAdapter.listenUsingRfcommWithServiceRecord(
                        "PhongPN Transfer Data Bluetooth Classic", UUID.fromString(
                            Contstants.UUID
                        )
                    )
            } catch (e: IOException) {
                Log.e("PhongPN", "Could not listen RFCOMM Sockets", e)
                e.printStackTrace()
            }
        }
    
        override fun run() {
            var socket: BluetoothSocket? = null
            while (socket == null) {
                try {
                    val message = Message.obtain()
                    message.what = Contstants.STATE_CONNECTING
                    handler.sendMessage(message)
                    socket = serverSocket?.accept()
                } catch (e: IOException) {
                    e.printStackTrace()
                    val message = Message.obtain()
                    message.what = Contstants.STATE_CONNECTION_FAILED
                    handler.sendMessage(message)
                }
                if (socket != null) {
                    val message = Message.obtain()
                    message.what = Contstants.STATE_CONNECTED
                    handler.sendMessage(message)
                    bluetoothSending = BluetoothSending(socket)
                    bluetoothSending.start()
                    break
                }
            }
        }
    
        fun cancel() {
            try {
                serverSocket?.close()
            } catch (e: IOException) {
                Log.e("PhongPN", "Could not close the connect socket", e)
                e.printStackTrace()
            }
        }
    }
    

    Cuối cùng, chúng ta tạo một Client tương tác và trao đổi dữ liệu với Server Socket.

    inner class ClientClass(device: BluetoothDevice) : Thread() {
    
        private var socket: BluetoothSocket? = null
    
        init {
            try {
                socket = device.createRfcommSocketToServiceRecord(UUID.fromString(Contstants.UUID))
            } catch (e: IOException) {
                Log.e("PhongPN", "Could not create RFCOMM Sockets", e)
                e.printStackTrace()
            }
        }
    
        override fun run() {
            try {
                socket?.connect()
                val message = Message.obtain()
                message.what = Contstants.STATE_CONNECTED
                handler.sendMessage(message)
                bluetoothSending = BluetoothSending(socket)
                bluetoothSending.start()
            } catch (e: IOException) {
                e.printStackTrace()
                val message = Message.obtain()
                message.what = Contstants.STATE_CONNECTION_FAILED
                handler.sendMessage(message)
            }
        }
    
        fun cancel() {
            try {
                socket?.close()
            } catch (e: IOException) {
                Log.e("PhongPN", "Could not close the connect socket", e)
                e.printStackTrace()
            }
        }
    }
    

    Chúng ta xử lý send data tới nơi hiển thị dữ liệu (View) qua đối tượng Handle.

    private val handler: Handler = object : Handler(Looper.getMainLooper()) {
        override fun handleMessage(message: Message) {
            when (message.what) {
                Contstants.STATE_LISTENING -> binding.txtStatus.text = "Listening"
                Contstants.STATE_CONNECTING -> binding.txtStatus.text = "Connecting"
                Contstants.STATE_CONNECTED -> binding.txtStatus.text = "Connected"
                Contstants.STATE_CONNECTION_FAILED -> binding.txtStatus.text = "Connection Failed"
                Contstants.STATE_MESSAGE_RECEIVED -> {
                    val readBuff = message.obj as ByteArray
                    val tempMsg = String(readBuff, 0, message.arg1)
                    binding.txtReceivedMessage.text = tempMsg
                }
            }
        }
    }
    

    Các bạn chú ý là phương thức close() của luồng cho phép bạn chấm dứt kết nối bất kỳ lúc nào bằng cách đóng BluetoothSocket. Các nên gọi phương thức này khi bạn sử dụng xong kết nối Bluetooth. Vì thế trong 2 class client vs server mình luôn có 2 hàm đó để sẵn sàng trong việc đóng kết nối.

    Trên đây là chuỗi bài viết của mình liên quan đến Bluetooth Classic trong Android. Mong đâu đấy chút chia sẻ của mình có thế giúp mọi người có cái nhìn sơ qua về Bluetooth Classic, cũng như là có thể ít nhiều giúp các bạn nếu có gặp phải bài toán liên quan đến nó trong tương lai.

    Cảm ơn mọi người đã theo dõi. Hẹn gặp lại trong các bài viết sắp tới.

  • Hướng dẫn cách Gen Document Design bằng code Python

    Hướng dẫn cách Gen Document Design bằng code Python

    Hi các bạn, có bao giờ các bạn gặp phải tình huống khi kết thúc dự án thì phải làm Document design cho dự án của mình? Yêu cầu phải liệt kê hết các func trong project ra file excel và giải thích nó làm gì. Nếu câu trả lời là có thì các bạn có thể đọc tiếp bài viết này để xem cách thực hiện nó như thế nào nhé.

    Yêu cầu

    • Cần liệt kê hết tất cả các func trong souce code và giải thích func đó dùng để làm gì
    • Định dạng yêu cầu theo file excel

    Chuẩn bị

    Do tool được code trên nền tảng python nên mọi người cần cài đặt Python3 trước để có thể thực hiện gen document. Để cài đặt thì mọi người có thể làm theo hướng dẫn sau:

    1. Homebrew

    Link tham khảo: https://brew.sh v/bin/bash -c “$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)”

    2. Install Python 3 on Mac – Brew Install

    Link tham khảo: https://www.freecodecamp.org/news/python-version-on-mac-update/ vbrew install pyenv // Install vpyenv install 3.9.2 // Update version

    3. Install pip: pip la một package của Python

    Link tham khảo: https://pip.pypa.io/en/stable/

    Download get-pip.py provided by https://pip.pypa.io hoặc sử dung terminal: vcurl https://bootstrap.pypa.io/get-pip.py -o get-pip.py vsudo python3 get-pip.py

    4. Openpyxl install: Cài đặt openpyxl sử dụng pip.

    Link tham khảo: https://openpyxl.readthedocs.io/en/stable/ vsudo pip install openpyxl

    5. Tải sẵn file template ở đây:

    File design là file excel template
    File DocC+.py là file code python nhằm mục đích gen gen souce và update file excel.

    NOTE: Đây là code dùng để gen dự án sử dụng ngôn ngữ lập trình swift, nếu các bạn cần gen trên ngôn ngữ khác chúng ta sẽ cần thực hiện chỉnh sửa file DocC+.py cho phù hợp với ngôn ngữ mà bạn sử dụng.

    Hướng dẫn sử dụng

    Bước 1: Copy file DocC+.py và design.xlsx vào trong thư mục cần gen.

    Bước 2: Bật Terminal -> navigate đến project folders chữa file DocC+.py

    Bước 3: Chạy câu lệnh dưới đây: python3 DocC+.py

    Vậy là chúng ta đã gen xong file liệt kê tất cả các func có trong thư mục mà các bạn chọn. Rất nhanh gọn và tiện lợi đúng không?

    Ưu điểm

    • Tiết kiệm rất nhiều thời gian thực hiện, thay vì phải copy bằng tay mất rất nhiều thời gian và khiến người thực hiện khá stress thì tool giúp chúng ta làm nó trong vài phút
    • Có thể sử dụng cho tất cả các ngôn ngữ lập trình
    • Quá dễ sử dụng

    Tổng kết

    Vậy là bài viết trên mình đã giới thiệu, chia sẻ và hướng dẫn các bạn sử dụng một tool cực kì hữu dụng và dễ sử dụng. Mình hi vọng nó sẽ giúp các bạn giải quyết bài toán mà bạn gặp phải. Chúc các bạn thành công!

  • Lottie Animation: Biến hình màn hình Login của bạn

    Lottie Animation: Biến hình màn hình Login của bạn

    Hi mọi người, ở bài viết trước mình đã hướng dẫn các bạn cách làm sao để thêm và ứng dụng Lottie vào ứng dụng của bạn. Có thể khi đọc bài viết trước các bạn sẽ chưa hình dung được sức mạnh của Lottie Animation. Để chứng minh sức mạnh của Lottie Animation hôm nay mình sẽ hướng dẫn các bạn ứng dụng nó vào màn hình Login và biến nó trở thành 1 màn hình thú vị hơn. Để hiểu rõ hơn về cách cài đặt Lottie animation bạn có thể xem lại bài viết Hướng dẫn cách sử dụng Lottie Animation trong Swift

    Chuẩn bị

    Để có thể bắt đầu dễ dàng và nhanh chóng hơn thì chúng ta cần chuẩn bị sẵn những thứ sau:

    1. Kiến thức về Lottie, tham khảo tại đây
    2. Project demo các bạn tải tại đây

    Bắt đầu

    Đầu tiên chúng ta sẽ kéo UI vào để thực hiện thay đổi màn hình Login như sau:

    Trong màn hình Login của chúng ta sẽ có các thành phần sau:

    1. ImageView làm background cho ứng dụng
    2. 2 Lottie Animation View, 1 cái làm thành phần chính nằm trên nửa màn hình và một cái nằm ở góc nhằm trang trí thêm cho màn hình
    3. 2 UITextField để cho người dùng nhập user name và password
    4. 1 button Login

    Sau khi sử dụng interface builder thực hiện kéo các view vào màn hình thì các bạn kéo reference cho các item vào view controller để thực hiện code.

    Mở ViewController lên và import Lottie

    import UIKit
    import Lottie

    Viết thêm một số func để cài đặt lottie và cài đặt view như sau

    extension ViewController {
        func setupUI() {
            tfAccount.layer.borderWidth = 1
            tfAccount.layer.borderColor = UIColor.orange.cgColor
            tfAccount.backgroundColor = UIColor.orange.withAlphaComponent(0.2)
            tfAccount.layer.cornerRadius = 5
            tfAccount.attributedPlaceholder = NSAttributedString(string: "  Account",
                                                                 attributes: [NSAttributedString.Key.foregroundColor: UIColor.white])
            tfAccount.placeholderRect(forBounds: CGRect(x: 5, y: 5, width: 100, height: 44))
            tfPassword.layer.borderWidth = 1
            tfPassword.layer.borderColor = UIColor.orange.cgColor
            tfPassword.backgroundColor = UIColor.orange.withAlphaComponent(0.2)
            tfPassword.layer.cornerRadius = 5
            tfPassword.attributedPlaceholder = NSAttributedString(string: "  Password",
                                                                 attributes: [NSAttributedString.Key.foregroundColor: UIColor.white])
            btnLogin.layer.cornerRadius = 5
            loadingView.backgroundColor = UIColor.black.withAlphaComponent(0.4)
        }
        
        func setupLottie() {
            // 1. Set animation content mode
            animationLottieView.contentMode = .scaleAspectFit
            animationLottieView2.contentMode = .scaleAspectFit
    
            // 2. Set animation loop mode
    
            animationLottieView.loopMode = .loop
            animationLottieView2.loopMode = .loop
    
            // 3. Adjust animation speed
    
            animationLottieView.animationSpeed = 0.5
            animationLottieView2.animationSpeed = 0.5
    
            // 4. Play animation
            animationLottieView.play()
            animationLottieView2.play()
        }
        
        private func showLoading() {
            // 1. Create lottie animation view
            let lottieView: LottieAnimationView = LottieAnimationView(name: "loading_crypto")
            lottieView.frame = CGRect(x: 0, y: 0, width: 100, height: 100)
            
            // 2. Set animation content mode
            lottieView.contentMode = .scaleAspectFit
            
            // 3. Set animation loop mode
            lottieView.loopMode = .loop
            
            // 4. Adjust animation speed
            lottieView.animationSpeed = 0.5
            
            loadingView.addSubview(lottieView)
            lottieView.center = CGPoint(x: UIScreen.main.bounds.width / 2, y:  UIScreen.main.bounds.height / 2)
            view.addSubview(loadingView)
            // 5. Play animation
            lottieView.play()
            
        }
        
        @objc private func hideLoading() {
            loadingView.subviews.forEach { $0.removeFromSuperview() }
            loadingView.removeFromSuperview()
        }
    }

    Thêm loading view để thực hiện màn hình loading như sau

    class ViewController: UIViewController {
        @IBOutlet weak var tfAccount: UITextField!
        @IBOutlet weak var tfPassword: UITextField!
        @IBOutlet weak var animationLottieView: LottieAnimationView!
        @IBOutlet weak var btnLogin: UIButton!
        @IBOutlet weak var animationLottieView2: LottieAnimationView!
        
        private var loadingView: UIView = UIView.init(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height))
        
        override func viewDidLoad() {
            super.viewDidLoad()
            // Do any additional setup after loading the view.
            setupUI()
            setupLottie()
        }
        
        @IBAction func login(_ sender: Any) {
            showLoading()
            perform(#selector(hideLoading), with: nil, afterDelay: 5)
        }
    }

    ở viewDidLoad chúng ta sẽ thực hiện setUpUI và setup lottie, tại func login chúng ta sẽ thực hiện showLoading và để nó ẩn đi sau 5 giây.

    Bây giờ chúng ta thực hiện cmd + R để chạy ứng dụng, kết quả thu được sẽ như sau:

    Kết quả thật tuyệt vời đúng không? Đối với các màn hình có animation phức tạp như vậy để code sử dụng animation gần như ta quá phức tạp và tốn quá nhiều effort để thực hiện, tuy nhiên nếu dùng lottie các bạn chỉ mất một chút thời gian để căn chỉnh layout.

    Vậy là chúng ta đã hoàn thành việc ứng dụng Lottie vào màn hình Login để tăng trải nghiệm của người dùng. Ngoài ra chúng ta còn có thể ứng dụng Lottie vào rất nhiều các tính năng khác để ứng dụng trở nên thú vị hơn. Ví dụ như: Màn hình launch screen với animation logo, animation trên notification, animation Tabbar icon, v.v.

    Cách sử dụng Lottie phụ thuộc rất lớn vào trí tưởng tượng và óc sáng tạo của người dùng, nếu bạn chỉ nắm vai trò là front end developer nhiều khi bạn sẽ không có quyền quyết định ứng dụng sẽ như nào, nhưng nếu bạn có ý tưởng hay ho bạn có thể đưa ý kiến lên Team leader, designer, BA, và PM của dự án để cải thiện trải nghiệm của người dùng.

    Chúc các bạn thành công với các dự án sắp tới.

  • ANDROID BLUETOOTH CLASSIC – PART 3

    ANDROID BLUETOOTH CLASSIC – PART 3

    Chào các bạn, hôm nay mình tiếp tục chuỗi bài liên quan đến Bluetooth Classic trong Android. Bài hôm nay mình tập trung vào phần Connect giữa các thiết bị Bluetooth.

    Để tạo kết nối Bluetooth giữa 2 thiết bị chúng ta cần thiết lập lập kết nối giống như việc kết nối giữa máy chủ (Server) và máy khách (Client). Chúng giao tiếp với nhau thông qua RFCOMM sockets, cụ thể thông qua đối tượng BluetoothSocket Socket trong Android. Cơ chế như nhau, máy khách (Client) cung cấp Socket information khi nó mở một RFCOMM Channel đến máy chủ (Server), máy chủ (Server) nhận Socket information khi kết nối đến được Accept. Server và Client được coi là đã kết nối với nhau khi chúng có một BluetoothSocket được kết nối trên cùng một kênh RFCOMM.

    Có 2 kết nối mà bạn cần quan tâm khi tạo Connect trong Bluetooth Classic

    • Connect như là một Client
    • Connect như là một Server

    Với từng bài toán, từng requirement cụ thể thì ta sẽ sử dụng chúng theo cách phù hợp.

    1. Connect như là một Client

      Cách connect đầu tiên này thường dùng cho bài toán kết nối với một thiết bị Bluetooth có sẵn. Từ đó kết nối, điều khiển, transfer dữ liệu với thiết bị đó. Đầu tiên bạn phải có được một đối tượng BluetoothDevice đại diện cho thiết bị Bluetooth. Đây là công đoạn bạn Scan (Discovery) và lấy thông thiên của thiết bị. Sau đó, bạn phải sử dụng BluetoothDevice để có được BluetoothSocket và bắt đầu kết nối.

      Các bước cụ thể như sau:

      1. Tạo một BluetoothSocket (RFCOMM Sockets) từ BluetoothDevice bằng cách gọi hàm createRfcommSocketToServiceRecord(UUID). Các bạn có hiểu UUID là một mã string, kiểu như một Port Number. Mã này của Client truyền vào hàm này, phải khớp với mã UUID mà máy chủ cấp. Thường với bài toán kết nối với thiết bị Bluetooth, thì thiết bị Bluetooth có cung cấp một chuỗi UUID chuẩn để ứng dụng có thể fix cứng dưới code và gửi lên khi tạo connect.

      2. Gọi BluetoothSocket.connect(). Sau khi gọi hàm, hệ thống sẽ thực hiện tra cứu SDP để tìm thiết bị từ xa có UUID phù hợp. Nếu tìm kiếm và khớp thành công và thiết bị từ xa chấp nhận kết nối, nó sẽ chia sẻ kênh RFCOMM để sử dụng trong quá trình kết nối và trả về phương thức connect(). Nếu kết nối không thành công hoặc nếu phương thức connect() timeout (sau khoảng 12 giây), thì phương thức này sẽ đưa ra một IOException. Bạn có thể tham khảo mã code sau

        class BluetoothClassicConnection(device: BluetoothDevice): Thread() {
        
        private val bluetoothSocket: BluetoothSocket? by lazy {
            device.createRfcommSocketToServiceRecord(UUID)
        }
        
        override fun run() {
            // Cancel discovery because it otherwise slows down the connection.
            if (BluetoothAdapter.getDefaultAdapter()?.isDiscovering == true) {
                BluetoothAdapter.getDefaultAdapter()?.cancelDiscovery()
            }
        
            try {
                bluetoothSocket?.let { socket ->
                    socket.connect()
        
                    // Handle Transfer Data 
                }
            } catch (e: IOException) {
                try {
                    bluetoothSocket?.close()
                } catch (e: IOException) {
                    Log.e(TAG, "Could not close the client socket", e)
                }
                Log.e(TAG, "Could not connect", e)
            }
        }
        
        // Closes the client socket and causes the thread to finish.
        fun cancel() {
            try {
                bluetoothSocket?.close()
            } catch (e: IOException) {
                Log.e(TAG, "Could not close the client socket", e)
            }
        }
        
        companion object {
            private const val UUID = "UUID code provide from Server Site"
         }
        }
        

      Note:

      • Việc duy trì kết nối Bluetooth Classic khá tốn tài nguyên của máy, đặc biệt như là PIN. Các bạn cần lưu ý đóng socket khi không cần dùng nữa, bằng cách gọi close() của đối tượng BluetoothSocket. Sau khi gọi Socket sẽ ngay lực tức được đóng lại và giải phóng tất cả các tài nguyên nội bộ có liên quan.
      • Chú ý đến việc huỷ Discovery() trước khi kết nối để đảm bảo việc connect được diễn ra thành công.
    2. Connect như một Server

      Với loại connect này được thực hiện nếu bạn muốn connect 2 thiết bị devices với nhau. Một thiết bị trong 2 thiết bị đó phải hoạt động như một Server bằng cách nắm giữ BluetoothServerSocket. Cụ thể BluetoothServerSocket lắng nghe các kết nối, sử dụng để thu nhận kết nối bluetoothSocket (RFCOMM sockets). Khi kết nối được thiết lập, máy chủ socket không còn cần thiết nữa và có thể đóng lại thông qua hàm close().

      Các bước cụ thể như sau:

      1. Lấy thông tin BluetoothServerSocket bằng cách gọi hàm listenUsingRfcommWithServiceRecord(String, UUID).

      2. Bắt đầu lắng nghe các yêu cầu kết nối bằng cách gọi accept(). Khi thành công, accept() trả về BluetoothSocket được kết nối và sau đó chúng ta sẽ sử dụng nó để trao đổi dữ liệu với thiết bị kết nối.

        class BluetoothClassicServerConnection: Thread() {
        private val serverSocket: BluetoothServerSocket? by lazy {
            BluetoothAdapter.getDefaultAdapter()?.listenUsingInsecureRfcommWithServiceRecord(NAME, UUID)
        }
        
        private var bluetoothSocket: BluetoothSocket? = null
        
        override fun run() {
            // Keep listening until exception occurs or a socket is returned.
            var shouldLoop = true
        
            while (shouldLoop) {
                val socket: BluetoothSocket? = try {
                    bluetoothSocket = serverSocket?.accept()
                } catch (e: IOException) {
                    Log.e(TAG, "Socket's accept() method failed", e)
                    shouldLoop = false
                    null
                }
                socket?.also {
                    
                    // Handle Transfer Data  
        
                    serverSocket?.close()
                    shouldLoop = false
                }
            }
        }
        
        // Closes the connect socket and causes the thread to finish.
        fun cancel() {
            try {
                serverSocket?.close()
            } catch (e: IOException) {
                Log.e(TAG, "Could not close the connect socket", e)
            }
        }
        
        companion object {
            private const val NAME = "The name we provide the service with when it is added to the SDP (Service Discovery Protocol)"
            private const val UUID = "UUID code provide from Server Site"
          }
        }
        

      Note:

      • Khi call accept() trả về BluetoothSocket, Socket đã được kết nối. Vì vậy, không nên gọi connect(), giống như mình làm ở phía Client.

    Vậy là mình cũng vừa trình bày xong phần liên quan đến Connect Bluetooth Classic trong Android. Các loại và các trường hợp sử dụng nó. Bài tiếp theo mình sẽ trình bày về việc Transfer Data (Read and Write) của Bluetooth Classic.

    Hẹn gặp lại các bạn trong bài viết sắp tới.

  • Android Bluetooth Classic – Part 2

    Android Bluetooth Classic – Part 2

    Tiếp tục trong chuỗi bài liên quan đến Bluetooth Classic trong Android. Mình xin chia sẻ phần liên quan đến Tìm kiếm (Scanning – Discovery) và Enable discoverability thiết bị.

    1. Tìm kiếm (Scanning – Discovery) thiết bị Bluetooth

      Để bắt đầu tìm kiếm (Scanning) thiết bị, hàm startDiscovery() thông qua đối tượng BluetoothAdapter được gọi. Quá trình này bất đồng bộ (asynchronous) và trả về một giá trị boolean cho biết quá trình discovery đã bắt đầu thành công hay chưa. Theo như mình tìm hiểu thì quá trình scan thường được truy vấn trong khoảng 12 giây, sau đó hiển thị output lên màn hình hoặc nơi hiển thị kết quả.

      Để nhận thông tin về từng thiết bị được quét (tìm thấy) thành công, ứng dụng của bạn phải register một BroadcastReceiver với ACTION_FOUND intent. Hệ thống sẽ broadcasts intent này cho từng thiết bị. Trong intent này sẽ chứa công tin về device thông qua các extra fields như EXTRA_DEVICE và EXTRA_CLASS.

      Cụ thể:

    class LegacyPairingFlowBluetoothDeviceReceiver(
        private val mListener: Listener
    ) : BroadcastReceiver() {
    
        companion object {
            private const val RESTART_DISCOVERY_DELAY = 5_000L // ms
        }
    
        private val handler = Handler(Looper.getMainLooper())
        private val restartDiscovery = Runnable {
            if (!cancelScan) {
                BluetoothAdapter.getDefaultAdapter()?.startDiscovery()
            }
        }
    
        var cancelScan: Boolean = false
            set(value) {
                field = value
                if (value) {
                    handler.removeCallbacks(restartDiscovery)
                }
            }
    
        override fun onReceive(context: Context, intent: Intent) {
            val state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR)
            if (state == BluetoothAdapter.STATE_ON && !cancelScan) {
                BluetoothAdapter.getDefaultAdapter()?.startDiscovery()
            }
    
            val action = intent.action
            if (BluetoothDevice.ACTION_FOUND == action) {
                val device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE) as? BluetoothDevice
                if (device != null) {
                    mListener.onFoundBluetoothDevice(device)
                }
            } else if (BluetoothAdapter.ACTION_DISCOVERY_STARTED == action) {
                mListener.onDiscoveryStarted()
            } else if (BluetoothAdapter.ACTION_DISCOVERY_FINISHED == action) {
                if (!cancelScan) {
                    handler.postDelayed(restartDiscovery, RESTART_DISCOVERY_DELAY)
                }
                mListener.onDiscoveryFinished()
            }
    
            if (BluetoothAdapter.STATE_ON == state) {
                mListener.onBluetoothStateOn()
            }
        }
    
        interface Listener {
            fun onFoundBluetoothDevice(device: BluetoothDevice)
    
            fun onDiscoveryStarted()
    
            fun onDiscoveryFinished()
    
            fun onBluetoothStateOn() {}
        }
    }
    
    Để bắt đầu kết nối với thiết bị Bluetooth, bạn gọi getAddress() của BluetoothDevice để truy xuất địa chỉ MAC được liên kết.
    

    Note: Thực hiện tìm kiếm (gọi discovery) thiết bị tiêu tốn rất nhiều tài nguyên. Sau khi bạn đã tìm thấy một thiết bị để kết nối, hãy chắc chắn rằng bạn dừng việc tìm kiếm lại bằng function cancelDiscovery() trước khi thử connect với thiết bị đó. Ngoài ra, bạn không nên thực hiện tìm kiếm (gọi discovery) khi được kết nối với thiết bị vì quá trình tìm kiếm làm giảm đáng kể băng thông khả dụng cho bất kỳ kết nối hiện có.

    1. Enable discoverability

      Nếu bạn chỉ cần kết nối từ ứng dụng của bạn với một thiết bị Bluetooth, transfer data với chúng thì bạn không cần phải Enable discoverability của thiết bị. Enable discoverability chỉ cần thiết khi bạn muốn ứng dụng của mình là một host a server socket chấp nhận các kết nối đến, vì các thiết bị Bluetooth phải có thể khám phá các thiết bị khác trước khi khởi động kết nối với các thiết bị khác.

      Để thực hiện điều đó, chúng ta gọi hàm startActivityForResult(Intent, int) với action Intent tương ứng là ACTION_REQUEST_DISCOVERABLE. Theo mặc định, thiết bị có thể được discover trong hai phút. Bạn có thể xác định thời lượng khác, tối đa một giờ, bằng cách thêm EXTRA_DISCOVERABLE_DURATION extra cho Intent đó.

        val requestCode = 1;
        val discoverableIntent: Intent = Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE).apply {
           putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, 500)
        }
        startActivityForResult(discoverableIntent, requestCode)
    

    Note: Nếu Bluetooth chưa được bật trên thiết bị, thì việc Enable discoverability sẽ tự động bật Bluetooth.

    Bài tiếp theo mình sẽ trình bày về việc Connect và Transfer giữa ứng dụng và thiết bị Bluetooth. Hẹn gặp lại các bạn ở bài viết sắp tới.