Tạo ứng dụng đa phương tiện cho ô tô

Sử dụng bộ sưu tập để sắp xếp ngăn nắp các trang Lưu và phân loại nội dung dựa trên lựa chọn ưu tiên của bạn.

Android Auto và Android Automotive OS giúp bạn đưa nội dung ứng dụng đa phương tiện của mình đến với người dùng trên ô tô của họ. Ứng dụng đa phương tiện cho ô tô phải cung cấp dịch vụ trình duyệt nội dung đa phương tiện để Android Auto và Android Automotive OS (hoặc một ứng dụng khác có trình duyệt nội dung đa phương tiện) có thể khám phá và hiển thị nội dung của bạn.

Hướng dẫn này giả định rằng bạn đã có một ứng dụng đa phương tiện phát âm thanh trên điện thoại và ứng dụng đa phương tiện đó phù hợp với cấu trúc ứng dụng đa phương tiện của Android.

Hướng dẫn này mô tả các thành phần bắt buộc của MediaBrowserServiceMediaSession mà ứng dụng của bạn cần để hoạt động trên Android Auto hoặc Android Automotive OS. Sau khi hoàn thành cơ sở hạ tầng đa phương tiện cốt lõi, bạn có thể thêm tính năng hỗ trợ Android Autothêm tính năng hỗ trợ Android Automotive OS vào ứng dụng đa phương tiện của mình.

Trước khi bạn bắt đầu

  1. Xem lại tài liệu về API Android Media.
  2. Xem lại Nguyên tắc thiết kế ứng dụng trên Android Automotive OSNguyên tắc thiết kế ứng dụng trên Android Auto.
  3. Xem lại các thuật ngữ và khái niệm chính được liệt kê trong phần này.

Các thuật ngữ và khái niệm chính

Dịch vụ trình duyệt nội dung đa phương tiện
Một dịch vụ Android do ứng dụng đa phương tiện của bạn triển khai và tuân thủ API MediaBrowserServiceCompat. Ứng dụng của bạn dùng dịch vụ này để hiển thị nội dung.
Trình duyệt nội dung đa phương tiện
Một API được các ứng dụng đa phương tiện dùng để khám phá dịch vụ trình duyệt nội dung đa phương tiện và hiển thị nội dung của các ứng dụng đó. Android Auto và Android Automotive OS dùng một trình duyệt nội dung đa phương tiện để tìm dịch vụ trình duyệt nội dung đa phương tiện của ứng dụng.
Mục nội dung đa phương tiện

Trình duyệt nội dung đa phương tiện sắp xếp nội dung theo cây đối tượng MediaItem. Một mục nội dung đa phương tiện có thể chứa một hoặc cả hai cờ sau:

  • Có thể phát: Cờ này cho biết mục là một lá trên cây nội dung. Mục này biểu thị một luồng âm thanh duy nhất, chẳng hạn như một bài hát trong đĩa nhạc, một chương trong sách nói hoặc một tập podcast.
  • Có thể xem: Cờ này cho biết mục là một nút trên cây nội dung và sẽ có phần tử con. Ví dụ: mục này biểu thị một đĩa nhạc và phần tử con của đĩa nhạc đó là các bài hát trong đĩa nhạc.

Mục nội dung đa phương tiện vừa có thể xem vừa có thể phát hoạt động như một danh sách phát. Bạn có thể chọn chính mục đó để phát tất cả các phần tử con, hoặc bạn có thể duyệt qua các phần tử con.

Được tối ưu hoá cho xe

Hoạt động trong một ứng dụng trên Android Automotive OS tuân thủ nguyên tắc thiết kế của Android Automotive OS. Giao diện cho các hoạt động này không được vẽ bằng Android Automotive OS, vì vậy, bạn phải đảm bảo rằng ứng dụng của mình tuân thủ các nguyên tắc thiết kế. Thông thường, nguyên tắc này bao gồm đích nhấn và kích thước phông chữ lớn hơn, hỗ trợ chế độ ban ngày và ban đêm, cũng như tỷ lệ tương phản cao hơn.

Chúng tôi chỉ cho phép hiển thị giao diện người dùng được tối ưu hoá cho xe khi Các hạn chế về trải nghiệm người dùng trên ô tô (CUXR) không có hiệu lực vì các giao diện này có thể cần người dùng chú ý hoặc tương tác thêm. CUXR không có hiệu lực khi ô tô dừng hoặc đỗ nhưng vẫn luôn có hiệu lực khi ô tô chuyển động.

Bạn không cần thiết kế hoạt động cho Android Auto vì Android Auto sẽ vẽ giao diện riêng được tối ưu hoá cho xe bằng cách sử dụng thông tin lấy từ dịch vụ trình duyệt nội dung đa phương tiện của bạn.

Định cấu hình tệp kê khai của ứng dụng

Trước khi có thể tạo dịch vụ trình duyệt nội dung đa phương tiện, bạn cần định cấu hình tệp kê khai của ứng dụng.

Khai báo dịch vụ trình duyệt nội dung đa phương tiện

Cả Android Auto và Android Automotive OS đều kết nối với ứng dụng của bạn thông qua dịch vụ trình duyệt nội dung đa phương tiện để duyệt qua các mục nội dung đa phương tiện. Bạn khai báo dịch vụ Trình duyệt nội dung đa phương tiện trong tệp kê khai để cho phép Android Auto và Android Automotive OS khám phá dịch vụ này cũng như kết nối với ứng dụng của bạn.

Đoạn mã sau đây cho biết cách khai báo dịch vụ trình duyệt nội dung đa phương tiện trong tệp kê khai của bạn. Bạn nên đưa mã này vào tệp kê khai cho mô-đun Android Automotive OS và tệp kê khai cho ứng dụng điện thoại.

<application>
    ...
    <service android:name=".MyMediaBrowserService"
             android:exported="true">
        <intent-filter>
            <action android:name="android.media.browse.MediaBrowserService"/>
        </intent-filter>
    </service>
    ...
</application>

Chỉ định biểu tượng ứng dụng

Bạn cần chỉ định một biểu tượng ứng dụng mà Android Auto và Android Automotive OS có thể dùng để biểu thị ứng dụng của bạn trong giao diện người dùng hệ thống.

Bạn có thể chỉ định một biểu tượng dùng để biểu thị ứng dụng của mình bằng cách sử dụng nội dung khai báo tệp kê khai sau:

<!--The android:icon attribute is used by Android Automotive OS-->
<application>
    ...
    android:icon="@mipmap/ic_launcher">
    ...
    <!--Used by Android Auto-->
    <meta-data android:name="com.google.android.gms.car.notification.SmallIcon"
               android:resource="@drawable/ic_auto_icon" />
    ...
</application>

Tạo dịch vụ trình duyệt nội dung đa phương tiện

Bạn tạo một dịch vụ trình duyệt nội dung đa phương tiện bằng cách mở rộng lớp MediaBrowserServiceCompat. Sau đó, cả Android Auto và Android Automotive OS đều có thể dùng dịch vụ của bạn để làm những việc sau:

  • Duyệt qua hệ thống phân cấp nội dung của ứng dụng để hiển thị trình đơn cho người dùng.
  • Lấy mã thông báo cho đối tượng MediaSessionCompat của ứng dụng để điều khiển chế độ phát âm thanh.

Quy trình dịch vụ trình duyệt nội dung đa phương tiện

Phần này mô tả cách Android Automotive OS và Android Auto tương tác với dịch vụ trình duyệt nội dung đa phương tiện của bạn trong quy trình làm việc thông thường của người dùng.

  1. Một người dùng chạy ứng dụng của bạn trên Android Automotive OS hoặc Android Auto.
  2. Android Automotive OS hoặc Android Auto kết nối với dịch vụ trình duyệt nội dung đa phương tiện của ứng dụng bằng phương thức onCreate(). Trong cách triển khai phương thức onCreate(), bạn phải tạo và đăng ký đối tượng MediaSessionCompat cũng như đối tượng gọi lại của nó.
  3. Android Automotive OS hoặc Android Auto gọi phương thức onGetRoot() của dịch vụ để lấy mục nội dung đa phương tiện gốc trong hệ thống phân cấp nội dung của bạn. Mục nội dung đa phương tiện gốc không hiển thị. Thay vào đó, mục này được dùng để truy xuất thêm nội dung từ ứng dụng của bạn.
  4. Android Automotive OS hoặc Android Auto gọi phương thức onLoadChildren() của dịch vụ để lấy phần tử con của mục nội dung đa phương tiện gốc. Android Automotive OS và Android Auto hiển thị các mục nội dung đa phương tiện này ở cấp cao nhất của mục nội dung. Hãy xem phần Cấu trúc trình đơn gốc trên trang này để biết thêm thông tin về những gì hệ thống dự kiến ở cấp độ này.
  5. Nếu người dùng chọn một mục nội dung đa phương tiện có thể xem, thì phương thức onLoadChildren() của dịch vụ sẽ được gọi lại để truy xuất phần tử con của mục trình đơn đã chọn.
  6. Nếu người dùng chọn một mục nội dung đa phương tiện có thể phát, thì Android Automotive OS hoặc Android Auto sẽ gọi phương thức gọi lại phiên phát nội dung đa phương tiện thích hợp để thực hiện thao tác đó.
  7. Nếu được ứng dụng của bạn hỗ trợ, người dùng cũng có thể tìm kiếm nội dung của bạn. Trong trường hợp này, Android Automotive OS hoặc Android Auto sẽ gọi phương thức onSearch() của dịch vụ.

Xây dựng hệ thống phân cấp nội dung

Android Auto và Android Automotive OS gọi dịch vụ trình duyệt nội dung đa phương tiện của ứng dụng để tìm hiểu xem nội dung nào có sẵn. Bạn cần triển khai 2 phương thức trong dịch vụ trình duyệt nội dung đa phương tiện để hỗ trợ việc này: onGetRoot()onLoadChildren().

Triển khai onGetRoot

Phương thức onGetRoot() của dịch vụ sẽ trả về thông tin liên quan đến nút gốc trong hệ thống phân cấp nội dung. Android Auto và Android Automotive OS dùng nút gốc này để yêu cầu nội dung còn lại bằng phương thức onLoadChildren().

Đoạn mã sau đây cho thấy cách triển khai đơn giản của phương thức onGetRoot():

Kotlin

override fun onGetRoot(
    clientPackageName: String,
    clientUid: Int,
    rootHints: Bundle?
): BrowserRoot? =
    // Verify that the specified package is allowed to access your
    // content! You'll need to write your own logic to do this.
    if (!isValid(clientPackageName, clientUid)) {
        // If the request comes from an untrusted package, return null.
        // No further calls will be made to other media browsing methods.

        null
    } else MediaBrowserServiceCompat.BrowserRoot(MY_MEDIA_ROOT_ID, null)

Java

@Override
public BrowserRoot onGetRoot(String clientPackageName, int clientUid,
    Bundle rootHints) {

    // Verify that the specified package is allowed to access your
    // content! You'll need to write your own logic to do this.
    if (!isValid(clientPackageName, clientUid)) {
        // If the request comes from an untrusted package, return null.
        // No further calls will be made to other media browsing methods.

        return null;
    }

    return new MediaBrowserServiceCompat.BrowserRoot(MY_MEDIA_ROOT_ID, null);
}

Để biết ví dụ chi tiết hơn về phương thức này, hãy xem phương thức onGetRoot() trong ứng dụng mẫu Universal Android Music Player trên GitHub.

Thêm phương thức xác thực gói cho onGetRoot()

Khi bạn thực hiện lệnh gọi đến phương thức onGetRoot() của dịch vụ, gói gọi sẽ chuyển thông tin nhận dạng đến dịch vụ của bạn. Dịch vụ của bạn có thể dùng thông tin này để quyết định xem gói đó có thể truy cập vào nội dung của bạn hay không. Ví dụ: bạn có thể chỉ cho phép các gói được phê duyệt truy cập vào nội dung của ứng dụng bằng cách so sánh clientPackageName với danh sách cho phép của bạn và xác minh chứng chỉ dùng để ký APK của gói đó. Nếu không thể xác minh gói, hãy trả về null để từ chối quyền truy cập vào nội dung của bạn.

Để cấp cho các ứng dụng hệ thống (chẳng hạn như Android Auto và Android Automotive OS) quyền truy cập vào nội dung của mình, dịch vụ của bạn phải luôn trả về một BrowserRoot không rỗng khi các ứng dụng hệ thống này gọi phương thức onGetRoot(). Chữ ký của ứng dụng hệ thống Android Automotive OS có thể khác nhau tuỳ theo nhà sản xuất và mẫu xe ô tô. Vì vậy, bạn nên cho phép các kết nối từ mọi ứng dụng hệ thống để hỗ trợ Android Automotive OS một cách mạnh mẽ.

Đoạn mã sau đây cho biết cách dịch vụ của bạn có thể xác thực rằng gói gọi là một ứng dụng hệ thống:

fun isKnownCaller(
    callingPackage: String,
    callingUid: Int
): Boolean {
    ...
    val isCallerKnown = when {
       // If the system is making the call, allow it.
       callingUid == Process.SYSTEM_UID -> true
       // If the app was signed by the same certificate as the platform
       // itself, also allow it.
       callerSignature == platformSignature -> true
       // ... more cases
    }
    return isCallerKnown
}

Đoạn mã này là phần trích dẫn từ lớp PackageValidator trong ứng dụng mẫu Universal Android Music Player trên GitHub. Hãy xem lớp đó để biết ví dụ chi tiết hơn về cách triển khai tính năng xác thực gói cho phương thức onGetRoot() của dịch vụ.

Ngoài việc cho phép các ứng dụng hệ thống, bạn phải cho phép Trợ lý Google kết nối với MediaBrowserService của mình. Lưu ý rằng Trợ lý Google có tên gói riêng cho điện thoại (bao gồm cả Android Auto) và cho Android Automotive OS.

Triển khai onLoadChildren()

Sau khi nhận được đối tượng nút gốc, Android Auto và Android Automotive OS sẽ tạo trình đơn cấp cao nhất bằng cách gọi onLoadChildren() trên đối tượng nút gốc để lấy phần tử con. Các ứng dụng khách xây dựng trình đơn con bằng cách gọi cùng một phương thức này thông qua đối tượng nút con.

Mỗi nút trong hệ thống phân cấp nội dung của bạn được biểu thị bằng một đối tượng MediaBrowserCompat.MediaItem. Mỗi mục nội dung đa phương tiện này được xác định bằng một chuỗi mã nhận dạng duy nhất. Ứng dụng khách coi những chuỗi mã này là mã thông báo mờ. Khi một ứng dụng khách muốn duyệt đến trình đơn con hoặc phát một mục nội dung đa phương tiện, ứng dụng đó sẽ chuyển mã thông báo. Ứng dụng của bạn chịu trách nhiệm liên kết mã thông báo với mục nội dung đa phương tiện thích hợp.

Lưu ý: Android Auto và Android Automotive OS có giới hạn nghiêm ngặt về số lượng mục nội dung đa phương tiện có thể hiển thị trong mỗi cấp độ của trình đơn. Những giới hạn này sẽ giảm thiểu sự phân tâm của người lái xe và giúp họ vận hành ứng dụng của bạn bằng lệnh thoại. Để biết thêm thông tin, hãy xem bài viết Duyệt qua chi tiết nội dungDuyệt qua thành phần hiển thị.

Đoạn mã sau đây cho thấy cách triển khai đơn giản của phương thức onLoadChildren():

Kotlin

override fun onLoadChildren(
    parentMediaId: String,
    result: Result<List<MediaBrowserCompat.MediaItem>>
) {
    // Assume for example that the music catalog is already loaded/cached.

    val mediaItems: MutableList<MediaBrowserCompat.MediaItem> = mutableListOf()

    // Check if this is the root menu:
    if (MY_MEDIA_ROOT_ID == parentMediaId) {

        // build the MediaItem objects for the top level,
        // and put them in the mediaItems list
    } else {

        // examine the passed parentMediaId to see which submenu we're at,
        // and put the children of that menu in the mediaItems list
    }
    result.sendResult(mediaItems)
}

Java

@Override
public void onLoadChildren(final String parentMediaId,
    final Result<List<MediaBrowserCompat.MediaItem>> result) {

    // Assume for example that the music catalog is already loaded/cached.

    List<MediaBrowserCompat.MediaItem> mediaItems = new ArrayList<>();

    // Check if this is the root menu:
    if (MY_MEDIA_ROOT_ID.equals(parentMediaId)) {

        // build the MediaItem objects for the top level,
        // and put them in the mediaItems list
    } else {

        // examine the passed parentMediaId to see which submenu we're at,
        // and put the children of that menu in the mediaItems list
    }
    result.sendResult(mediaItems);
}

Để biết ví dụ hoàn chỉnh về phương thức này, hãy xem phương thức onLoadChildren() trong ứng dụng mẫu Universal Android Music Player trên GitHub.

Cấu trúc trình đơn gốc

Hình 1. Nội dung gốc được hiển thị ở dạng thẻ điều hướng

Android Auto và Android Automotive OS có những hạn chế cụ thể về cấu trúc của trình đơn gốc. Các hạn chế này được truyền đến MediaBrowserService thông qua gợi ý gốc. Hệ thống có thể đọc gợi ý gốc thông qua đối số Bundle được chuyển vào onGetRoot(). Khi làm theo các gợi ý này, hệ thống có thể hiển thị nội dung gốc một cách tối ưu ở dạng thẻ điều hướng. Nếu bạn không làm theo các gợi ý này, hệ thống có thể bỏ hoặc ẩn bớt một số nội dung gốc. 2 gợi ý được gửi:

  1. Giới hạn về số lượng phần tử con gốc: trong hầu hết các trường hợp, bạn có thể thấy số lượng là 4. Nghĩa là không thể hiển thị hơn 4 thẻ.
  2. Cờ được hỗ trợ trên phần tử con gốc: bạn có thể thấy giá trị này là MediaItem#FLAG_BROWSABLE. Nghĩa là chỉ các mục có thể xem mới hiển thị ở dạng thẻ, còn các mục có thể phát thì không.

Hãy dùng mã sau để đọc các gợi ý gốc liên quan:

Kotlin

import androidx.media.utils.MediaConstants

// Later, in your MediaBrowserServiceCompat
override fun onGetRoot(
    clientPackageName: String,
    clientUid: Int,
    rootHints: Bundle
): BrowserRoot {

  val maximumRootChildLimit = rootHints.getInt(
      MediaConstants.BROWSER_ROOT_HINTS_KEY_ROOT_CHILDREN_LIMIT,
      /* defaultValue= */ 4)
  val supportedRootChildFlags = rootHints.getInt(
      MediaConstants.BROWSER_ROOT_HINTS_KEY_ROOT_CHILDREN_SUPPORTED_FLAGS,
      /* defaultValue= */ MediaItem.FLAG_BROWSABLE)

  // rest of method..
}

Java

import androidx.media.utils.MediaConstants;

// Later, in your MediaBrowserServiceCompat
@Override
public BrowserRoot onGetRoot(
    String clientPackageName, int clientUid, Bundle rootHints) {

    int maximumRootChildLimit = rootHints.getInt(
        MediaConstants.BROWSER_ROOT_HINTS_KEY_ROOT_CHILDREN_LIMIT,
        /* defaultValue= */ 4);
    int supportedRootChildFlags = rootHints.getInt(
        MediaConstants.BROWSER_ROOT_HINTS_KEY_ROOT_CHILDREN_SUPPORTED_FLAGS,
        /* defaultValue= */ MediaItem.FLAG_BROWSABLE);

    // rest of method...
}

Bạn có thể chọn phân nhánh logic cho cấu trúc của hệ thống phân cấp nội dung dựa trên giá trị của các gợi ý này, đặc biệt là nếu hệ thống phân cấp của bạn khác nhau giữa các hoạt động tích hợp MediaBrowser bên ngoài Android Auto và Android Automotive OS. Ví dụ: nếu thường hiển thị một mục gốc có thể phát, bạn nên lồng mục đó vào một mục gốc có thể xem, do giá trị của gợi ý về cờ được hỗ trợ.

Ngoài gợi ý gốc, bạn cần tuân thủ một số nguyên tắc bổ sung để đảm bảo các thẻ hiển thị tối ưu:

  1. Cung cấp biểu tượng đơn sắc (tốt nhất là màu trắng) cho từng mục trong thẻ.
  2. Cung cấp các nhãn ngắn gọn mà ý nghĩa cho từng mục trong thẻ. Việc ghi nhãn ngắn gọn sẽ giảm nguy cơ bị cắt bớt chuỗi.

Hiển thị hình minh hoạ nội dung đa phương tiện

Hình minh hoạ cho các mục nội dung đa phương tiện phải được chuyển ở dạng URI cục bộ bằng ContentResolver.SCHEME_CONTENT hoặc ContentResolver.SCHEME_ANDROID_RESOURCE. URI cục bộ này phải phân giải đến tệp bitmap hoặc vectơ vẽ được trong tài nguyên của ứng dụng. Đối với đối tượng MediaDescriptionCompat biểu thị các mục trong hệ thống phân cấp nội dung, hãy chuyển URI thông qua setIconUri(). Đối với đối tượng MediaMetadataCompat biểu thị mục đang phát, hãy chuyển URI qua putString() bằng cách sử dụng bất kỳ khoá nào sau đây:

Ví dụ sau minh hoạ cách tải hình ảnh xuống từ một URI web và hiển thị hình ảnh đó thông qua URI cục bộ. Để biết ví dụ hoàn chỉnh hơn, hãy xem cách triển khai openFile() và các phương thức xung quanh trong ứng dụng mẫu Universal Android Music Player.

  1. Tạo một URI content:// tương ứng với URI web. Dịch vụ trình duyệt nội dung đa phương tiện và phiên phát nội dung đa phương tiện sẽ chuyển URI nội dung này cho Android Auto và Android Automotive OS.

    Kotlin

    fun Uri.asAlbumArtContentURI(): Uri {
      return Uri.Builder()
        .scheme(ContentResolver.SCHEME_CONTENT)
        .authority(CONTENT_PROVIDER_AUTHORITY)
        .appendPath(this.getPath()) // make sure you trust the URI!
        .build()
    }
    

    Java

    public static Uri asAlbumArtContentURI(Uri webUri) {
      return new Uri.Builder()
        .scheme(ContentResolver.SCHEME_CONTENT)
        .authority(CONTENT_PROVIDER_AUTHORITY)
        .appendPath(webUri.getPath()) // make sure you trust the URI!
        .build();
    }
    
  2. Trong quá trình triển khai ContentProvider.openFile(), hãy kiểm tra xem một tệp có tồn tại cho URI tương ứng hay không. Nếu không, hãy tải xuống và lưu tệp hình ảnh vào bộ nhớ đệm (đoạn mã sau đây sử dụng Glide).

    Kotlin

    override fun openFile(uri: Uri, mode: String): ParcelFileDescriptor? {
      val context = this.context ?: return null
      val file = File(context.cacheDir, uri.path)
      if (!file.exists()) {
        val remoteUri = Uri.Builder()
            .scheme("https")
            .authority("my-image-site")
            .appendPath(uri.path)
            .build()
        val cacheFile = Glide.with(context)
            .asFile()
            .load(remoteUri)
            .submit()
            .get(DOWNLOAD_TIMEOUT_SECONDS, TimeUnit.SECONDS)
    
        cacheFile.renameTo(file)
        file = cacheFile
      }
      return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY)
    }
    

    Java

    @Nullable
    @Override
    public ParcelFileDescriptor openFile(@NonNull Uri uri, @NonNull String mode)
        throws FileNotFoundException {
      Context context = this.getContext();
      File file = new File(context.getCacheDir(), uri.getPath());
      if (!file.exists()) {
        Uri remoteUri = new Uri.Builder()
            .scheme("https")
            .authority("my-image-site")
            .appendPath(uri.getPath())
            .build();
        File cacheFile = Glide.with(context)
            .asFile()
            .load(remoteUri)
            .submit()
            .get(DOWNLOAD_TIMEOUT_SECONDS, TimeUnit.SECONDS);
    
        cacheFile.renameTo(file);
        file = cacheFile;
      }
      return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
    }
    

Để biết thêm thông tin chi tiết về trình cung cấp nội dung, hãy tham khảo phần Tạo trình cung cấp nội dung.

Áp dụng kiểu nội dung

Sau khi xây dựng hệ thống phân cấp nội dung bằng cách sử dụng các mục có thể xem hoặc có thể phát, bạn có thể áp dụng kiểu nội dung giúp xác định cách các mục đó hiển thị trên ô tô.

Bạn có thể dùng các kiểu nội dung sau:

Mục trong danh sách

Kiểu nội dung này ưu tiên tiêu đề và siêu dữ liệu hơn hình ảnh.

Mục trong lưới

Kiểu nội dung này ưu tiên hình ảnh hơn tiêu đề và siêu dữ liệu.

Đặt kiểu nội dung mặc định

Bạn có thể đặt giá trị mặc định chung cho cách hiển thị mục nội dung đa phương tiện bằng việc đưa các hằng số nhất định vào gói dữ liệu bổ sung BrowserRoot cho phương thức onGetRoot() của dịch vụ. Android Auto và Android Automotive OS sẽ đọc gói này và tìm kiếm các hằng số đó để xác định kiểu phù hợp.

Bạn có thể dùng các dữ liệu bổ sung sau làm khoá trong gói:

Các khoá có thể liên kết với những giá trị hằng số nguyên sau đây để ảnh hưởng đến cách trình bày các mục đó:

  • DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_LIST_ITEM: các mục tương ứng sẽ được trình bày ở dạng mục trong danh sách.
  • DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_GRID_ITEM: các mục tương ứng sẽ được trình bày ở dạng mục trong lưới.
  • DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_CATEGORY_LIST_ITEM: các mục tương ứng sẽ được trình bày ở dạng mục danh sách "danh mục". Các mục này giống với mục danh sách thông thường, ngoại trừ việc sẽ áp dụng lề xung quanh biểu tượng của mục, vì biểu tượng trông đẹp hơn khi có kích thước nhỏ. Các biểu tượng phải là các vectơ vẽ được có thể phủ màu. Gợi ý này sẽ chỉ được cung cấp cho các mục có thể xem.
  • DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_CATEGORY_GRID_ITEM: các mục tương ứng sẽ được trình bày ở dạng mục lưới "danh mục". Các mục này giống với mục lưới thông thường, ngoại trừ việc sẽ áp dụng lề xung quanh biểu tượng của mục, vì biểu tượng trông đẹp hơn khi có kích thước nhỏ. Các biểu tượng phải là các vectơ vẽ được có thể phủ màu. Gợi ý này sẽ chỉ được cung cấp cho các mục có thể xem.

Đoạn mã sau đây cho biết cách đặt kiểu nội dung mặc định cho mục có thể xem thành lưới và mục có thể phát thành danh sách:

Kotlin

import androidx.media.utils.MediaConstants

@Nullable
override fun onGetRoot(
    @NonNull clientPackageName: String,
    clientUid: Int,
    @Nullable rootHints: Bundle
): BrowserRoot {
    val extras = Bundle()
    extras.putInt(
        MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_BROWSABLE,
        MediaConstants.DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_GRID_ITEM)
    extras.putInt(
        MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_PLAYABLE,
        MediaConstants.DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_LIST_ITEM)
    return BrowserRoot(ROOT_ID, extras)
}

Java

import androidx.media.utils.MediaConstants;

@Nullable
@Override
public BrowserRoot onGetRoot(
    @NonNull String clientPackageName,
    int clientUid,
    @Nullable Bundle rootHints) {
    Bundle extras = new Bundle();
    extras.putInt(
        MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_BROWSABLE,
        MediaConstants.DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_GRID_ITEM);
    extras.putInt(
        MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_PLAYABLE,
        MediaConstants.DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_LIST_ITEM);
    return new BrowserRoot(ROOT_ID, extras);
}

Đặt kiểu nội dung cho từng mục

API Kiểu nội dung cho phép bạn ghi đè kiểu nội dung mặc định cho mọi phần tử con của mục nội dung đa phương tiện có thể xem, cũng như cho chính mục nội dung đa phương tiện bất kỳ.

Để ghi đè giá trị mặc định cho phần tử con của một mục nội dung đa phương tiện có thể xem, hãy tạo một gói dữ liệu bổ sung trong MediaDescription của mục nội dung đa phương tiện đó rồi thêm gợi ý tương tự như mô tả ở trên. DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_BROWSABLE áp dụng cho phần tử con có thể phát của mục đó, còn DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_PLAYABLE áp dụng cho phần tử con có thể xem của mục đó.

Để ghi đè giá trị mặc định cho chính mục nội dung đa phương tiện cụ thể (không phải phần tử con), hãy tạo gói dữ liệu bổ sung trong MediaDescription của mục nội dung đa phương tiện rồi thêm gợi ý bằng khoá DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_SINGLE_ITEM. Hãy dùng chính các giá trị mô tả ở trên để chỉ định cách trình bày mục đó.

Đoạn mã sau đây cho biết cách tạo một MediaItem có thể xem ghi đè kiểu nội dung mặc định cho chính nó và phần tử con. Mục này sẽ định kiểu chính nó là một mục danh sách danh mục, phần tử con có thể xem là mục trong danh sách và phần tử con có thể phát là mục trong lưới:

Kotlin

import androidx.media.utils.MediaConstants

private fun createBrowsableMediaItem(
    mediaId: String,
    folderName: String,
    iconUri: Uri
): MediaBrowser.MediaItem {
    val mediaDescriptionBuilder = MediaDescription.Builder()
    mediaDescriptionBuilder.setMediaId(mediaId)
    mediaDescriptionBuilder.setTitle(folderName)
    mediaDescriptionBuilder.setIconUri(iconUri)
    val extras = Bundle()
    extras.putInt(
        MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_SINGLE_ITEM,
        MediaConstants.DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_CATEGORY_LIST_ITEM)
    extras.putInt(
        MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_BROWSABLE,
        MediaConstants.DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_LIST_ITEM)
    extras.putInt(
        MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_PLAYABLE,
        MediaConstants.DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_GRID_ITEM)
    mediaDescriptionBuilder.setExtras(extras)
    return MediaBrowser.MediaItem(
        mediaDescriptionBuilder.build(), MediaBrowser.MediaItem.FLAG_BROWSABLE)
}

Java

import androidx.media.utils.MediaConstants;

private MediaBrowser.MediaItem createBrowsableMediaItem(
    String mediaId,
    String folderName,
    Uri iconUri) {
    MediaDescription.Builder mediaDescriptionBuilder = new MediaDescription.Builder();
    mediaDescriptionBuilder.setMediaId(mediaId);
    mediaDescriptionBuilder.setTitle(folderName);
    mediaDescriptionBuilder.setIconUri(iconUri);
    Bundle extras = new Bundle();
    extras.putInt(
        MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_SINGLE_ITEM,
        MediaConstants.DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_CATEGORY_LIST_ITEM);
    extras.putInt(
        MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_BROWSABLE,
        MediaConstants.DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_LIST_ITEM);
    extras.putInt(
        MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_PLAYABLE,
        MediaConstants.DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_GRID_ITEM);
    mediaDescriptionBuilder.setExtras(extras);
    return new MediaBrowser.MediaItem(
        mediaDescriptionBuilder.build(), MediaBrowser.MediaItem.FLAG_BROWSABLE);
}

Nhóm các mục bằng gợi ý tiêu đề

Để nhóm các mục nội dung đa phương tiện có liên quan lại với nhau, bạn có thể dùng gợi ý cho từng mục. Mọi mục nội dung đa phương tiện trong một nhóm đều cần khai báo gói dữ liệu bổ sung trong MediaDescription chứa dữ liệu liên kết với khoá DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE và một giá trị chuỗi giống hệt. Chuỗi này được dùng làm tiêu đề của nhóm và sẽ được bản địa hoá.

Đoạn mã sau đây cho biết cách tạo MediaItem có tiêu đề nhóm con là "Songs":

Kotlin

import androidx.media.utils.MediaConstants

private fun createMediaItem(
    mediaId: String,
    folderName: String,
    iconUri: Uri
): MediaBrowser.MediaItem {
    val mediaDescriptionBuilder = MediaDescription.Builder()
    mediaDescriptionBuilder.setMediaId(mediaId)
    mediaDescriptionBuilder.setTitle(folderName)
    mediaDescriptionBuilder.setIconUri(iconUri)
    val extras = Bundle()
    extras.putString(
        MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE,
        "Songs")
    mediaDescriptionBuilder.setExtras(extras)
    return MediaBrowser.MediaItem(
        mediaDescriptionBuilder.build(), /* playable or browsable flag*/)
}

Java

import androidx.media.utils.MediaConstants;

private MediaBrowser.MediaItem createMediaItem(String mediaId, String folderName, Uri iconUri) {
   MediaDescription.Builder mediaDescriptionBuilder = new MediaDescription.Builder();
   mediaDescriptionBuilder.setMediaId(mediaId);
   mediaDescriptionBuilder.setTitle(folderName);
   mediaDescriptionBuilder.setIconUri(iconUri);
   Bundle extras = new Bundle();
   extras.putString(
       MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE,
       "Songs");
   mediaDescriptionBuilder.setExtras(extras);
   return new MediaBrowser.MediaItem(
       mediaDescriptionBuilder.build(), /* playable or browsable flag*/);
}

Ứng dụng của bạn phải chuyển tất cả các mục nội dung đa phương tiện mà bạn muốn nhóm lại với nhau dưới dạng khối liền kề. Ví dụ: giả sử bạn muốn hiển thị 2 nhóm mục nội dung đa phương tiện: "Bài hát" và "Đĩa nhạc" (theo thứ tự đó), và ứng dụng của bạn đã chuyển 5 mục nội dung đa phương tiện theo thứ tự sau:

  1. Mục nội dung đa phương tiện A với extras.putString(MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE, "Songs")
  2. Mục nội dung đa phương tiện B với extras.putString(MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE, "Albums")
  3. Mục nội dung đa phương tiện C với extras.putString(MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE, "Songs")
  4. Mục nội dung đa phương tiện D với extras.putString(MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE, "Songs")
  5. Mục nội dung đa phương tiện E với extras.putString(MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE, "Albums")

Các mục nội dung đa phương tiện trong nhóm "Bài hát" và "Đĩa nhạc" không được lưu giữ cùng nhau trong khối liền kề, do đó, Android Auto và Android Automotive OS sẽ hiểu thành 4 nhóm sau đây:

  • Nhóm 1 có tên là "Bài hát" chứa mục nội dung đa phương tiện A
  • Nhóm 2 có tên là "Đĩa nhạc" chứa mục nội dung đa phương tiện B
  • Nhóm 3 có tên là "Bài hát" chứa các mục nội dung đa phương tiện C và D
  • Nhóm 4 có tên là "Đĩa nhạc" chứa mục nội dung đa phương tiện E

Để hiển thị các mục này trong 2 nhóm, ứng dụng của bạn sẽ chuyển các ứng dụng theo thứ tự sau:

  1. Mục nội dung đa phương tiện A với extras.putString(MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE, "Songs")
  2. Mục nội dung đa phương tiện C với extras.putString(MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE, "Songs")
  3. Mục nội dung đa phương tiện D với extras.putString(MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE, "Songs")
  4. Mục nội dung đa phương tiện B với extras.putString(MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE, "Albums")
  5. Mục nội dung đa phương tiện E với extras.putString(MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE, "Albums")

Hiển thị chỉ báo siêu dữ liệu bổ sung

Bạn có thể thêm chỉ báo siêu dữ liệu bổ sung để cung cấp thông tin nhanh về nội dung trong cây trình duyệt nội dung đa phương tiện và trong khi phát. Trong cây duyệt qua, Android Auto và Android Automotive OS sẽ đọc các dữ liệu bổ sung liên kết với một mục và tìm kiếm các hằng số nhất định để xác định những chỉ báo sẽ hiển thị. Trong khi phát nội dung đa phương tiện, Android Auto và Android Automotive OS sẽ đọc siêu dữ liệu của phiên phát nội dung đa phương tiện đó và tìm kiếm các hằng số nhất định để xác định những chỉ báo sẽ hiển thị.

Hình 2. Thành phần hiển thị phát có siêu dữ liệu xác định bài hát và nghệ sĩ, cũng như một biểu tượng cho biết nội dung tục tĩu

Hình 3. Thành phần hiển thị duyệt qua có một dấu chấm cho nội dung chưa phát trên mục đầu tiên, và một thanh tiến trình cho nội dung đã phát một phần trên mục thứ hai

Các hằng số sau đây có thể được dùng trong cả dữ liệu bổ sung mô tả MediaItem lẫn dữ liệu bổ sung MediaMetadata:

Hằng số sau có thể chỉ được dùng trong dữ liệu bổ sung mô tả MediaItem:

Để hiển thị các chỉ báo xuất hiện khi người dùng đang duyệt qua cây trình duyệt nội dung đa phương tiện, hãy tạo một gói dữ liệu bổ sung bao gồm một hoặc nhiều hằng số này và chuyển gói đó cho phương thức MediaDescription.Builder.setExtras().

Đoạn mã sau đây cho biết cách hiển thị chỉ báo cho một mục nội dung đa phương tiện tục tĩu đã phát 70%:

Kotlin

import androidx.media.utils.MediaConstants

val extras = Bundle()
extras.putLong(
    MediaConstants.METADATA_KEY_IS_EXPLICIT,
    MediaConstants.METADATA_VALUE_ATTRIBUTE_PRESENT)
extras.putInt(
    MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS,
    MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_PARTIALLY_PLAYED)
extras.putDouble(
    MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_PERCENTAGE, 0.7)
val description =
    MediaDescriptionCompat.Builder()
        .setMediaId(/*...*/)
        .setTitle(resources.getString(/*...*/))
        .setExtras(extras)
        .build()
return MediaBrowserCompat.MediaItem(description, /* flags */)

Java

import androidx.media.utils.MediaConstants;

Bundle extras = new Bundle();
extras.putLong(
    MediaConstants.METADATA_KEY_IS_EXPLICIT,
    MediaConstants.METADATA_VALUE_ATTRIBUTE_PRESENT);
extras.putInt(
    MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS,
    MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_PARTIALLY_PLAYED);
extras.putDouble(
    MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_PERCENTAGE, 0.7);
MediaDescriptionCompat description =
    new MediaDescriptionCompat.Builder()
        .setMediaId(/*...*/)
        .setTitle(resources.getString(/*...*/))
        .setExtras(extras)
        .build();
return new MediaBrowserCompat.MediaItem(description, /* flags */);

Để hiển thị chỉ báo cho một mục nội dung đa phương tiện đang phát, bạn có thể khai báo các giá trị Long cho METADATA_KEY_IS_EXPLICIT hoặc EXTRA_DOWNLOAD_STATUS trong MediaMetadataCompat của mediaSession. Bạn không thể hiển thị chỉ báo DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS hoặc DESCRIPTION_EXTRAS_KEY_COMPLETION_PERCENTAGE trong thành phần hiển thị phát.

Đoạn mã sau đây minh hoạ cách cho biết rằng bài hát hiện tại trong thành phần hiển thị phát là tục tĩu và đã được tải xuống:

Kotlin

import androidx.media.utils.MediaConstants

mediaSession.setMetadata(
    MediaMetadataCompat.Builder()
        .putString(
            MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, "Song Name")
        .putString(
            MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE, "Artist name")
        .putString(
            MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI,
            albumArtUri.toString())
        .putLong(
            MediaConstants.METADATA_KEY_IS_EXPLICIT,
            MediaConstants.METADATA_VALUE_ATTRIBUTE_PRESENT)
        .putLong(
            MediaDescriptionCompat.EXTRA_DOWNLOAD_STATUS,
            MediaDescriptionCompat.STATUS_DOWNLOADED)
        .build())

Java

import androidx.media.utils.MediaConstants;

mediaSession.setMetadata(
    new MediaMetadataCompat.Builder()
        .putString(
            MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, "Song Name")
        .putString(
            MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE, "Artist name")
        .putString(
            MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI,
            albumArtUri.toString())
        .putLong(
            MediaConstants.METADATA_KEY_IS_EXPLICIT,
            MediaConstants.METADATA_VALUE_ATTRIBUTE_PRESENT)
        .putLong(
            MediaDescriptionCompat.EXTRA_DOWNLOAD_STATUS,
            MediaDescriptionCompat.STATUS_DOWNLOADED)
        .build());

Cập nhật thanh tiến trình trong thành phần hiển thị duyệt qua khi đang phát nội dung

Như mô tả ở trên, bạn có thể dùng dữ liệu bổ sung DESCRIPTION_EXTRAS_KEY_COMPLETION_PERCENTAGE để hiển thị thanh tiến trình cho nội dung được phát một phần trong thành phần hiển thị duyệt qua. Tuy nhiên, nếu người dùng tiếp tục phát nội dung đã phát một phần từ Android Auto hoặc Android Automotive OS, thì chỉ báo đó sẽ không chính xác khi thời gian trôi qua. Để Android Auto và Android Automotive OS luôn cập nhật thanh tiến trình, bạn có thể cung cấp thêm thông tin trong MediaMetadataCompatPlaybackStateCompat để liên kết nội dung hiện tại với các mục nội dung đa phương tiện trong thành phần hiển thị duyệt qua. Các yêu cầu sau đây phải được đáp ứng thì mục nội dung đa phương tiện mới có thanh tiến trình cập nhật tự động:

Đoạn mã sau đây minh hoạ cách cho biết rằng mục đang phát được liên kết với một mục trong thành phần hiển thị duyệt qua:

Kotlin

import androidx.media.utils.MediaConstants

// When the MediaItem is constructed to show in the browse view
// Suppose the item was 25% complete when the user launched the browse view
val mediaItemExtras = Bundle()
mediaItemExtras.putDouble(
    MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_PERCENTAGE, 0.25)
val description =
    MediaDescriptionCompat.Builder()
        .setMediaId("my-media-id")
        .setExtras(mediaItemExtras)
        // ...and any other setters
        .build()
return MediaBrowserCompat.MediaItem(description, /* flags */)

// Elsewhere, when the user has selected MediaItem for playback
mediaSession.setMetadata(
    MediaMetadataCompat.Builder()
        .putString(MediaMetadata.METADATA_KEY_MEDIA_ID, "my-media-id")
        // ...and any other setters
        .build())

val playbackStateExtras = Bundle()
playbackStateExtras.putString(
    MediaConstants.PLAYBACK_STATE_EXTRAS_KEY_MEDIA_ID, "my-media-id")
mediaSession.setPlaybackState(
    PlaybackStateCompat.Builder()
        .setExtras(playbackStateExtras)
        // ...and any other setters
        .build())

Java

import androidx.media.utils.MediaConstants;

// When the MediaItem is constructed to show in the browse view
// Suppose the item was 25% complete when the user launched the browse view
Bundle mediaItemExtras = new Bundle();
mediaItemExtras.putDouble(
    MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_PERCENTAGE, 0.25);
MediaDescriptionCompat description =
    new MediaDescriptionCompat.Builder()
        .setMediaId("my-media-id")
        .setExtras(mediaItemExtras)
        // ...and any other setters
        .build();
return MediaBrowserCompat.MediaItem(description, /* flags */);

// Elsewhere, when the user has selected MediaItem for playback
mediaSession.setMetadata(
    new MediaMetadataCompat.Builder()
        .putString(MediaMetadata.METADATA_KEY_MEDIA_ID, "my-media-id")
        // ...and any other setters
        .build());

Bundle playbackStateExtras = new Bundle();
playbackStateExtras.putString(
    MediaConstants.PLAYBACK_STATE_EXTRAS_KEY_MEDIA_ID, "my-media-id");
mediaSession.setPlaybackState(
    new PlaybackStateCompat.Builder()
        .setExtras(playbackStateExtras)
        // ...and any other setters
        .build());

Hình 4. Thành phần hiển thị phát có tuỳ chọn "Kết quả tìm kiếm" để xem các mục nội dung đa phương tiện liên quan đến nội dung tìm kiếm bằng giọng nói của người dùng

Ứng dụng của bạn có thể cung cấp kết quả tìm kiếm theo ngữ cảnh được hiển thị với người dùng khi họ bắt đầu một truy vấn tìm kiếm. Android Auto và Android Automotive OS sẽ hiển thị các kết quả này thông qua giao diện truy vấn tìm kiếm hoặc thông qua các thành phần hướng đến truy vấn đã thực hiện trước đó trong phiên phát. Để tìm hiểu thêm, hãy xem phần Hỗ trợ thao tác bằng giọng nói trên trang này.

Để hiển thị kết quả tìm kiếm có thể xem, bạn nên thêm khoá hằng số BROWSER_SERVICE_EXTRAS_KEY_SEARCH_SUPPORTED vào gói dữ liệu bổ sung cho phương thức onGetRoot() của dịch vụ, ánh xạ đến boolean true.

Đoạn mã sau đây cho biết cách bật tính năng hỗ trợ trong phương thức onGetRoot():

Kotlin

import androidx.media.utils.MediaConstants

@Nullable
fun onGetRoot(
    @NonNull clientPackageName: String,
    clientUid: Int,
    @Nullable rootHints: Bundle
): BrowserRoot {
    val extras = Bundle()
    extras.putBoolean(
        MediaConstants.BROWSER_SERVICE_EXTRAS_KEY_SEARCH_SUPPORTED, true)
    return BrowserRoot(ROOT_ID, extras)
}

Java

import androidx.media.utils.MediaConstants;

@Nullable
@Override
public BrowserRoot onGetRoot(
    @NonNull String clientPackageName,
    int clientUid,
    @Nullable Bundle rootHints) {
    Bundle extras = new Bundle();
    extras.putBoolean(
        MediaConstants.BROWSER_SERVICE_EXTRAS_KEY_SEARCH_SUPPORTED, true);
    return new BrowserRoot(ROOT_ID, extras);
}

Để bắt đầu cung cấp kết quả tìm kiếm, hãy ghi đè phương thức onSearch() trong dịch vụ trình duyệt nội dung đa phương tiện của bạn. Android Auto và Android Automotive OS sẽ chuyển tiếp các cụm từ tìm kiếm của người dùng đến phương thức này bất cứ khi nào người dùng gọi giao diện truy vấn tìm kiếm hoặc thành phần "Kết quả tìm kiếm". Bạn có thể sắp xếp kết quả tìm kiếm từ phương thức onSearch() của dịch vụ bằng cách sử dụng mục tiêu đề để giúp kết quả dễ xem hơn. Ví dụ: nếu ứng dụng của bạn phát nhạc, thì bạn có thể sắp xếp kết quả tìm kiếm theo "Đĩa nhạc", "Nghệ sĩ" và "Bài hát".

Đoạn mã sau đây cho thấy cách triển khai đơn giản của phương thức onSearch():

Kotlin

fun onSearch(query: String, extras: Bundle) {
  // Detach from results to unblock the caller (if a search is expensive)
  result.detach()
  object:AsyncTask() {
    internal var searchResponse:ArrayList
    internal var succeeded = false
    protected fun doInBackground(vararg params:Void):Void {
      searchResponse = ArrayList()
      if (doSearch(query, extras, searchResponse))
      {
        succeeded = true
      }
      return null
    }
    protected fun onPostExecute(param:Void) {
      if (succeeded)
      {
        // Sending an empty List informs the caller that there were no results.
        result.sendResult(searchResponse)
      }
      else
      {
        // This invokes onError() on the search callback
        result.sendResult(null)
      }
      return null
    }
  }.execute()
}
// Populates resultsToFill with search results. Returns true on success or false on error
private fun doSearch(
    query: String,
    extras: Bundle,
    resultsToFill: ArrayList
): Boolean {
  // Implement this method
}

Java

@Override
public void onSearch(final String query, final Bundle extras,
                        Result<List<MediaItem>> result) {

  // Detach from results to unblock the caller (if a search is expensive)
  result.detach();

  new AsyncTask<Void, Void, Void>() {
    List<MediaItem> searchResponse;
    boolean succeeded = false;
    @Override
    protected Void doInBackground(Void... params) {
      searchResponse = new ArrayList<MediaItem>();
      if (doSearch(query, extras, searchResponse)) {
        succeeded = true;
      }
      return null;
    }

    @Override
    protected void onPostExecute(Void param) {
      if (succeeded) {
       // Sending an empty List informs the caller that there were no results.
       result.sendResult(searchResponse);
      } else {
        // This invokes onError() on the search callback
        result.sendResult(null);
      }
    }
  }.execute()
}

/** Populates resultsToFill with search results. Returns true on success or false on error */
private boolean doSearch(String query, Bundle extras, ArrayList<MediaItem> resultsToFill) {
    // Implement this method
}

Bật bộ điều khiển chế độ phát

Android Auto và Android Automotive OS sẽ gửi các lệnh điều khiển chế độ phát thông qua MediaSessionCompat của dịch vụ. Bạn phải đăng ký một phiên phát và triển khai các phương thức gọi lại liên quan.

Đăng ký một phiên phát nội dung đa phương tiện

Trong phương thức onCreate() của dịch vụ trình duyệt nội dung đa phương tiện, hãy tạo MediaSessionCompat rồi đăng ký phiên phát nội dung đa phương tiện bằng cách gọi setSessionToken().

Đoạn mã sau đây cho biết cách tạo và đăng ký một phiên phát nội dung đa phương tiện:

Kotlin

override fun onCreate() {
    super.onCreate()

    ...
    // Start a new MediaSession
    val session = MediaSessionCompat(this, "session tag").apply {
        // Set a callback object to handle play control requests, which
        // implements MediaSession.Callback
        setCallback(MyMediaSessionCallback())
    }
    sessionToken = session.sessionToken

    ...
}

Java

public void onCreate() {
    super.onCreate();

    ...
    // Start a new MediaSession
    MediaSessionCompat session = new MediaSessionCompat(this, "session tag");
    setSessionToken(session.getSessionToken());

    // Set a callback object to handle play control requests, which
    // implements MediaSession.Callback
    session.setCallback(new MyMediaSessionCallback());

    ...
}

Khi tạo đối tượng phiên phát nội dung đa phương tiện, bạn sẽ đặt đối tượng gọi lại được dùng để xử lý các yêu cầu điều khiển chế độ phát. Bạn tạo đối tượng gọi lại này bằng cách cung cấp cách triển khai lớp MediaSessionCompat.Callback cho ứng dụng của mình. Phần tiếp theo sẽ thảo luận về cách triển khai đối tượng này.

Triển khai lệnh phát

Khi người dùng yêu cầu phát một mục nội dung đa phương tiện trên ứng dụng của bạn, Android Automotive OS và Android Auto sẽ dùng lớp MediaSessionCompat.Callback từ đối tượng MediaSessionCompat của ứng dụng mà họ đã nhận được từ dịch vụ trình duyệt nội dung đa phương tiện của ứng dụng. Khi người dùng muốn điều khiển chế độ phát nội dung, chẳng hạn như tạm dừng phát hoặc chuyển sang bản nhạc tiếp theo, Android Auto và Android Automotive OS sẽ gọi một trong các phương thức của đối tượng gọi lại.

Để xử lý chế độ phát nội dung, ứng dụng của bạn phải mở rộng lớp trừu tượng MediaSessionCompat.Callback và triển khai các phương thức mà ứng dụng đó hỗ trợ.

Bạn nên triển khai tất cả các phương thức gọi lại sau đây nếu phù hợp với loại nội dung mà ứng dụng của bạn cung cấp:

onPrepare()
Được gọi khi nguồn nội dung đa phương tiện thay đổi. Android Automotive OS cũng gọi phương thức này ngay sau khi khởi động. Ứng dụng đa phương tiện của bạn phải triển khai phương thức này.
onPlay()
Được gọi nếu người dùng chọn phát mà không chọn một mục cụ thể. Ứng dụng của bạn sẽ phát nội dung mặc định. Nếu chế độ phát đã bị tạm dừng bằng onPause(), ứng dụng của bạn sẽ tiếp tục phát.

Lưu ý: Ứng dụng của bạn sẽ không tự động bắt đầu phát nhạc khi Android Automotive OS hoặc Android Auto kết nối với dịch vụ trình duyệt nội dung đa phương tiện. Để biết thêm thông tin, hãy xem phần Đặt trạng thái phát ban đầu.

onPlayFromMediaId()
Được gọi khi người dùng chọn phát một mục cụ thể. Phương thức này được chuyển mã nhận dạng mà dịch vụ trình duyệt nội dung đa phương tiện đã chỉ định cho mục nội dung đa phương tiện trong hệ thống phân cấp nội dung của bạn.
onPlayFromSearch()
Được gọi khi người dùng chọn phát từ một truy vấn tìm kiếm. Ứng dụng sẽ đưa ra lựa chọn phù hợp dựa trên chuỗi tìm kiếm đã chuyển vào.
onPause()
Được gọi khi người dùng chọn tạm dừng phát.
onSkipToNext()
Được gọi khi người dùng chọn chuyển sang mục tiếp theo.
onSkipToPrevious()
Được gọi khi người dùng chọn chuyển về mục trước đó.
onStop()
Được gọi khi người dùng chọn ngừng phát.

Ứng dụng của bạn sẽ ghi đè các phương thức này để cung cấp chức năng bạn muốn. Bạn không cần phải triển khai một phương thức nếu ứng dụng của bạn không hỗ trợ phương thức đó. Ví dụ: nếu ứng dụng của bạn phát trực tiếp (chẳng hạn như chương trình phát sóng thể thao), thì phương thức onSkipToNext() sẽ không phù hợp để triển khai. Thay vào đó, bạn có thể dùng cách triển khai mặc định của onSkipToNext().

Ứng dụng của bạn không cần logic đặc biệt nào để phát nội dung qua loa trên ô tô. Khi nhận được một yêu cầu phát nội dung, ứng dụng của bạn sẽ phát âm thanh như cách thông thường (ví dụ: phát nội dung qua loa hoặc tai nghe của người dùng). Android Auto và Android Automotive OS sẽ tự động gửi nội dung âm thanh đến hệ thống của ô tô để phát qua loa của ô tô.

Để biết thêm thông tin về cách phát nội dung âm thanh, hãy xem bài viết Phát nội dung đa phương tiện, Quản lý chế độ phát âm thanhExoPlayer.

Đặt thao tác phát tiêu chuẩn

Android Auto và Android Automotive OS sẽ hiển thị bộ điều khiển chế độ phát dựa trên các thao tác được bật trong đối tượng PlaybackStateCompat.

Theo mặc định, ứng dụng của bạn phải hỗ trợ những thao tác sau:

Ứng dụng của bạn có thể hỗ trợ thêm các thao tác sau đây nếu phù hợp với nội dung của ứng dụng:

Ngoài ra, bạn nên tạo một hàng đợi phát có thể hiển thị với người dùng. Để làm việc này, bạn cần gọi phương thức setQueue()setQueueTitle(), bật thao tác ACTION_SKIP_TO_QUEUE_ITEM và xác định lệnh gọi lại onSkipToQueueItem().

Android Auto và Android Automotive OS sẽ hiển thị các nút cho mỗi thao tác đã bật, cũng như hàng đợi phát nếu bạn chọn tạo một hàng đợi. Khi các nút được nhấp, hệ thống sẽ gọi lệnh gọi lại tương ứng từ MediaSessionCompat.Callback.

Đặt trước không gian không sử dụng

Android Auto và Android Automotive OS sẽ đặt trước không gian trong giao diện người dùng cho các thao tác ACTION_SKIP_TO_PREVIOUSACTION_SKIP_TO_NEXT. Nếu ứng dụng của bạn không hỗ trợ một trong các chức năng này, thì Android Auto và Android Automotive OS sẽ sử dụng không gian để hiển thị mọi thao tác tuỳ chỉnh mà bạn tạo.

Nếu không muốn các thao tác tuỳ chỉnh lấp đầy không gian đó, thì bạn có thể đặt trước không gian. Bằng cách này, Android Auto và Android Automotive OS sẽ để trống không gian bất cứ khi nào ứng dụng của bạn không hỗ trợ chức năng tương ứng. Để làm vậy, hãy gọi phương thức setExtras() bằng một gói dữ liệu bổ sung chứa hằng số tương ứng với các chức năng đã đặt trước. SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_NEXT tương ứng với ACTION_SKIP_TO_NEXTSESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_PREV tương ứng với ACTION_SKIP_TO_PREVIOUS. Hãy dùng các hằng số này làm khoá trong gói và sử dụng boolean true làm giá trị.

Đặt PlaybackState ban đầu

Khi Android Auto và Android Automotive OS kết nối với dịch vụ trình duyệt nội dung đa phương tiện của bạn, phiên phát nội dung đa phương tiện sẽ thông báo trạng thái phát nội dung bằng PlaybackStateCompat. Ứng dụng của bạn sẽ không tự động bắt đầu phát nhạc khi Android Automotive OS hoặc Android Auto kết nối với dịch vụ trình duyệt nội dung đa phương tiện. Thay vào đó, hãy để Android Auto và Android Automotive tiếp tục hoặc bắt đầu phát dựa trên trạng thái của ô tô hoặc các thao tác của người dùng.

Để thực hiện việc này, hãy đặt PlaybackStateCompat ban đầu của phiên phát nội dung đa phương tiện thành STATE_STOPPED, STATE_PAUSED, STATE_NONE hoặc STATE_ERROR.

Các phiên phát nội dung đa phương tiện trong Android Auto và Android Automotive OS chỉ kéo dài trong thời hạn của ổ đĩa. Vì vậy, người dùng sẽ thường xuyên bắt đầu và dừng các phiên phát này. Để tạo ra trải nghiệm liền mạch giữa các ổ đĩa, hãy theo dõi trạng thái phiên phát trước đó của người dùng (ví dụ: mục nội dung đa phương tiện phát gần đây nhất, PlaybackStateCompat và hàng đợi). Bằng cách này, khi ứng dụng đa phương tiện nhận được yêu cầu tiếp tục, người dùng có thể tự động tiếp tục từ nơi đã dừng lại.

Thêm các thao tác phát tuỳ chỉnh

Bạn có thể thêm thao tác phát tuỳ chỉnh để hiển thị các thao tác bổ sung mà ứng dụng đa phương tiện của bạn hỗ trợ. Nếu không gian cho phép (và không được đặt trước), Android sẽ thêm các thao tác tuỳ chỉnh vào bộ điều khiển truyền tải. Nếu không, thao tác tuỳ chỉnh sẽ hiển thị trong trình đơn mục bổ sung. Các thao tác tuỳ chỉnh sẽ hiển thị theo thứ tự được thêm vào PlaybackStateCompat.

Thao tác tuỳ chỉnh sẽ cung cấp hành vi riêng biệt với thao tác chuẩn và không được dùng để thay thế hoặc sao chép thao tác chuẩn.

Bạn có thể thêm thao tác tuỳ chỉnh bằng cách sử dụng phương thức addCustomAction() trong lớp PlaybackStateCompat.Builder.

Đoạn mã sau đây cho biết cách thêm một thao tác tuỳ chỉnh có tên "Bắt đầu kênh radio":

Kotlin

stateBuilder.addCustomAction(
    PlaybackStateCompat.CustomAction.Builder(
        CUSTOM_ACTION_START_RADIO_FROM_MEDIA,
        resources.getString(R.string.start_radio_from_media),
        startRadioFromMediaIcon
    ).run {
        setExtras(customActionExtras)
        build()
    }
)

Java

stateBuilder.addCustomAction(
    new PlaybackStateCompat.CustomAction.Builder(
        CUSTOM_ACTION_START_RADIO_FROM_MEDIA,
        resources.getString(R.string.start_radio_from_media),
        startRadioFromMediaIcon)
    .setExtras(customActionExtras)
    .build());

Để biết ví dụ chi tiết hơn về phương thức này, hãy xem phương thức setCustomAction() trong ứng dụng mẫu Universal Android Music Player trên GitHub.

Sau khi tạo thao tác tuỳ chỉnh, phiên phát nội dung đa phương tiện của bạn có thể phản hồi thao tác đó bằng cách ghi đè phương thức onCustomAction().

Đoạn mã sau đây cho biết cách ứng dụng của bạn có thể phản hồi một thao tác "Bắt đầu kênh radio":

Kotlin

override fun onCustomAction(action: String, extras: Bundle?) {
    when(action) {
        CUSTOM_ACTION_START_RADIO_FROM_MEDIA -> {
            ...
        }
    }
}

Java

@Override
public void onCustomAction(@NonNull String action, Bundle extras) {
    if (CUSTOM_ACTION_START_RADIO_FROM_MEDIA.equals(action)) {
        ...
    }
}

Để biết ví dụ chi tiết hơn về phương thức này, hãy xem phương thức onCustomAction trong ứng dụng mẫu Universal Android Music Player trên GitHub.

Biểu tượng cho các thao tác tuỳ chỉnh

Mỗi thao tác tuỳ chỉnh mà bạn tạo cần phải có một tài nguyên biểu tượng. Các ứng dụng trên ô tô có thể chạy trên nhiều kích thước và mật độ màn hình. Vì vậy, các biểu tượng bạn cung cấp phải là vectơ vẽ được. Vectơ vẽ được cho phép bạn mở rộng thành phần mà không làm mất chi tiết. Vectơ vẽ được cũng giúp dễ dàng căn chỉnh cạnh và góc theo ranh giới điểm ảnh ở độ phân giải nhỏ hơn.

Nếu một thao tác tuỳ chỉnh có trạng thái (ví dụ: thao tác bật/tắt chế độ phát), hãy cung cấp biểu tượng cho từng trạng thái để người dùng có thể nhận thấy rõ sự thay đổi khi chọn thao tác đó.

Cung cấp kiểu biểu tượng thay thế cho các thao tác bị vô hiệu hoá

Với những trường hợp mà một thao tác tuỳ chỉnh không dùng được cho ngữ cảnh hiện tại, hãy hoán đổi biểu tượng thao tác tuỳ chỉnh bằng một biểu tượng thay thế cho biết rằng thao tác đó bị vô hiệu hoá.

Hình 5. Biểu tượng mẫu cho thao tác tuỳ chỉnh không theo kiểu

Hỗ trợ thao tác bằng giọng nói

Ứng dụng đa phương tiện của bạn phải hỗ trợ thao tác bằng giọng nói để mang đến cho người lái xe trải nghiệm an toàn và thuận tiện giúp giảm thiểu sự phân tâm. Ví dụ: nếu ứng dụng của bạn đang phát một mục nội dung đa phương tiện, thì người dùng có thể nói "Phát [tên bài hát] "(ví dụ: "Phát Bohemian Rhapsody") để yêu cầu ứng dụng đó phát một bài hát khác mà không cần nhìn hoặc chạm vào màn hình ô tô. Người dùng có thể bắt đầu truy vấn bằng cách nhấp vào các nút thích hợp trên vô lăng hoặc nói to cụm từ kích hoạt "Ok Google").

Khi Android Auto hoặc Android Automotive OS phát hiện và diễn giải một thao tác bằng giọng nói, thao tác bằng giọng nói đó sẽ được gửi đến ứng dụng thông qua onPlayFromSearch(). Khi nhận được lệnh gọi lại này, ứng dụng sẽ tìm nội dung khớp với chuỗi query và bắt đầu phát.

Người dùng có thể chỉ định nhiều danh mục từ khoá trong truy vấn: thể loại, nghệ sĩ, đĩa nhạc, tên bài hát, đài phát thanh hoặc danh sách phát, v.v. Khi xây dựng tính năng hỗ trợ tìm kiếm, hãy tính đến tất cả các danh mục phù hợp với ứng dụng của bạn. Nếu Android Auto hoặc Android Automotive OS phát hiện thấy rằng một truy vấn cụ thể phù hợp với các danh mục nhất định, thì các dữ liệu bổ sung sẽ được thêm vào tham số extras. Hệ thống có thể gửi các dữ liệu bổ sung sau:

Ứng dụng đa phương tiện của bạn nên tính đến một chuỗi query trống. Android Auto hoặc Android Automotive OS có thể gửi chuỗi này nếu người dùng không chỉ định cụm từ tìm kiếm (ví dụ: người dùng nói "Phát nhạc"). Trong trường hợp đó, ứng dụng của bạn có thể chọn bắt đầu một bản nhạc được phát gần đây hoặc bản nhạc mới đề xuất.

Nếu hệ thống không thể xử lý nhanh nội dung tìm kiếm, đừng chặn trong onPlayFromSearch(). Thay vào đó, hãy đặt trạng thái phát là STATE_CONNECTING rồi tìm kiếm trên luồng không đồng bộ.

Sau khi bắt đầu phát, hãy cân nhắc việc điền sẵn nội dung có liên quan vào hàng đợi của phiên phát nội dung đa phương tiện. Ví dụ: nếu người dùng yêu cầu phát một đĩa nhạc, thì ứng dụng của bạn có thể điền danh sách các bản nhạc trong đĩa nhạc đó vào hàng đợi này. Ngoài ra, hãy cân nhắc việc triển khai tính năng hỗ trợ kết quả tìm kiếm có thể xem để người dùng có thể chọn một bản nhạc khác khớp với truy vấn của họ.

Ngoài truy vấn "phát", Android Auto và Android Automotive OS cũng sẽ nhận dạng các truy vấn bằng giọng nói để điều khiển chế độ phát như "tạm dừng nhạc" và "bài hát tiếp theo", sau đó sẽ khớp những lệnh này với lệnh gọi lại phiên phát nội dung đa phương tiện thích hợp, chẳng hạn như onPause()onSkipToNext().

Để biết ví dụ chi tiết về cách triển khai các thao tác phát bằng giọng nói trong ứng dụng của bạn, hãy xem bài viết Trợ lý Google và ứng dụng đa phương tiện.

Triển khai các biện pháp bảo vệ để tránh sự phân tâm

Điện thoại của người dùng được kết nối với loa của ô tô trong khi sử dụng Android Auto, do đó, bạn phải có các biện pháp phòng ngừa bổ sung giúp tránh sự phân tâm của người lái xe.

Chặn chuông báo trên ô tô

Các ứng dụng đa phương tiện trên Android Auto không được bắt đầu phát âm thanh qua loa trên ô tô trừ khi người dùng muốn bắt đầu phát (ví dụ: bằng cách nhấn vào phát trong ứng dụng của bạn). Ngay cả chuông báo do người dùng lên lịch trên ứng dụng đa phương tiện cũng không được bắt đầu phát nhạc qua loa trên ô tô. Để đáp ứng yêu cầu này, ứng dụng của bạn có thể dùng CarConnection làm tín hiệu trước khi phát âm thanh bất kỳ. Ứng dụng của bạn có thể kiểm tra xem điện thoại có đang chiếu tới màn hình ô tô hay không bằng cách quan sát loại kết nối ô tô qua LiveData và kiểm tra xem loại kết nối đó có phải là CONNECTION_TYPE_PROJECTION hay không.

Nếu điện thoại của người dùng đang chiếu, thì các ứng dụng đa phương tiện hỗ trợ chuông báo phải thực hiện một trong những việc sau:

  • Tắt chuông báo.
  • Phát chuông báo qua STREAM_ALARM và cung cấp một giao diện người dùng trên màn hình điện thoại để tắt chuông báo.

Xử lý quảng cáo trong nội dung đa phương tiện

Theo mặc định, Android Auto sẽ hiển thị thông báo khi siêu dữ liệu đa phương tiện thay đổi trong phiên phát âm thanh. Khi một ứng dụng đa phương tiện chuyển từ phát nhạc sang chạy quảng cáo, việc hiển thị thông báo cho người dùng sẽ gây phân tâm (và không cần thiết). Để ngăn Android Auto hiển thị thông báo trong trường hợp này, bạn phải đặt khoá siêu dữ liệu đa phương tiện METADATA_KEY_IS_ADVERTISEMENT thành METADATA_VALUE_ATTRIBUTE_PRESENT, như minh hoạ trong đoạn mã sau:

Kotlin

import androidx.media.utils.MediaConstants

override fun onPlayFromMediaId(mediaId: String, extras: Bundle?) {
    MediaMetadataCompat.Builder().apply {
        if (isAd(mediaId)) {
            putLong(
                MediaConstants.METADATA_KEY_IS_ADVERTISEMENT,
                MediaConstants.METADATA_VALUE_ATTRIBUTE_PRESENT)
        }
        // ...add any other properties as you normally would
        mediaSession.setMetadata(build())
    }
}

Java

import androidx.media.utils.MediaConstants;

@Override
public void onPlayFromMediaId(String mediaId, Bundle extras) {
    MediaMetadataCompat.Builder builder = new MediaMetadataCompat.Builder();
    if (isAd(mediaId)) {
        builder.putLong(
            MediaConstants.METADATA_KEY_IS_ADVERTISEMENT,
            MediaConstants.METADATA_VALUE_ATTRIBUTE_PRESENT);
    }
    // ...add any other properties as you normally would
    mediaSession.setMetadata(builder.build());
}

Xử lý lỗi chung

Khi ứng dụng gặp lỗi, bạn nên đặt trạng thái phát là STATE_ERROR và đưa ra thông báo lỗi bằng phương thức setErrorMessage(). Thông báo lỗi phải hiển thị với người dùng và được bản địa hoá theo ngôn ngữ hiện tại của người dùng. Sau đó, Android Auto và Android Automotive OS có thể hiển thị thông báo lỗi với người dùng.

Để biết thêm thông tin về trạng thái lỗi, hãy xem phần Làm việc với phiên phát nội dung đa phương tiện: Trạng thái và lỗi.

Nếu người dùng Android Auto cần mở ứng dụng điện thoại để giải quyết lỗi, thì thông báo của bạn nên cung cấp thông tin đó cho người dùng. Ví dụ: thông báo lỗi của bạn sẽ có nội dung là "Đăng nhập vào [tên ứng dụng của bạn]" thay cho "Vui lòng đăng nhập".

Tài nguyên khác