Try the AppCard codelab

This page describes how to implement an AppCard.

Step 1: Add all the imports

  1. To add the imports:

    static_libs: [
         …
         "car-app-card",
    ],
    

Step 2: Add SimpleAppCardContentProvider to your manifest

  1. Be sure to replace android:authorities (in bold) with your package name.

    <!-- App Card Content Provider that is required to communicate
         relevant App Cards with the system
         android.car.permission.BIND_APP_CARD_PROVIDER is an essential permission
         that will only allow the system to communicate with the
         AppCardContentProvider
         The content provider must also be exported and enabled -->
         <provider android:name=".SimpleAppCardContentProvider"
             android:authorities="com.example.appcard.sample" 
             android:permission="@string/host_permission"
             android:exported="true"
             android:enabled="true">
             <intent-filter>
                <!-- This intent filter will allow the system to find and
                     connect with the application's AppCardContentProvider -->
                 <action android:name="com.android.car.appcard.APP_CARD_PROVIDER" />
                </intent-filter>
         </provider>
    

Step 3: Add SimpleAppCardContentProvider to the manifest

  1. To add SimpleAppCardContentProvider to the manifest:

    class SimpleAppCardContentProvider : AppCardContentProvider() {
      /** Must return same authority as defined in manifest */
      override val authority: String = AUTHORITY
    
      /** Setup [AppCardContentProvider] and its constituents */
      override fun onCreate(): Boolean {
        return super.onCreate()
      }
    
      /** Setup an [AppCard] that is being requested */
      override fun onAppCardAdded(id: String, ctx: AppCardContext): AppCard {
        return when (id) {
          APPCARD_ID -> //TODO: create app card
    
          else -> throw IllegalStateException("Unidentified app card ID: $id")
        }
      }
    
      /** List of supported [AppCard] IDs */
       override val appCardIds: List<String> = listOf(APPCARD_ID).toMutableList()
    
      /** Clean up when an [AppCard] is removed */
      override fun onAppCardRemoved(id: String) {
        when (id) {
          APPCARD_ID -> //TODO: create app card
        }
      }
    
      /** Handle an [AppCardContext] change for a particular [AppCard] ID */
      override fun onAppCardContextChanged(
        id: String,
        appCardContext: AppCardContext
      ) {
         when (id) {
          APPCARD_ID -> //TODO: update AppCardContext
        }
      }
    
      companion object {
        private const val AUTHORITY = "com.example.appcard.sample"
        private const val APPCARD_ID = "sampleAppCard"
      }
    }
    

Step 4: Create an AppCard

  1. To create an AppCard:

    override fun onAppCardAdded(id: String, ctx: AppCardContext): AppCard {
      return when (id) {
        APPCARD_ID -> createAppCard(ctx)
    
        else -> throw IllegalStateException("Unidentified app card ID: $id")
      }
    }
    
    private fun createAppCard(appCardContext: AppCardContext): ImageAppCard {
      return ImageAppCard.newBuilder(APPCARD_ID)
        .setPrimaryText("Hello")
        .setSecondaryText("World")
        .setHeader(
          Header.newBuilder("header")
            .setTitle("Code Lab")
            .build()
        )
        .addButton(
          Button.newBuilder(
            "button",
            Button.ButtonType.PRIMARY,
            object : OnClickListener {
              override fun onClick() {
                //no-op
              }
            }
          )
            .setText("Click me!")
            .build()
        )
       .build()
    }
    

For example:

Create an AppCard

Figure 1. Create an AppCard.

Step 5: Enable the button clicker

To enable the clicker:

   private var clickCounter = 0

   private fun createAppCard(appCardContext: AppCardContext): ImageAppCard {
      ...
       .addButton(
        Button.newBuilder(
          "button",
           Button.ButtonType.PRIMARY,
           object : OnClickListener {
             override fun onClick() {
               clickCounter++
               sendAppCardUpdate(createAppCard(appCardContext))
             }
           }
         )
           .setText(
             if (clickCounter == 0) "Click me!" else "Clicked: $clickCounter"
           )
           .build()
         )
      ...
    }

    override fun onAppCardRemoved(id: String) {
      when (id) {
        APPCARD_ID -> clickCounter = 0
      }
    }

For example:

Enable the button clicker

Figure 2. Enable the button clicker.

Step 6: Update the header image

  1. To update the image in the header:

      private fun createAppCard(appCardContext: AppCardContext): ImageAppCard {
        val headerImageSize = appCardContext.imageAppCardContext.getMaxImageSize(Header::class.java)
        val logo = resToBitmap(
         android.R.drawable.ic_menu_compass,
           headerImageSize.width,
           headerImageSize.height
        )
    
     ...
       .setHeader(
         Header.newBuilder("header")
           .setTitle("Code Lab")
           .setImage(
             Image.newBuilder("image")
               .setContentScale(Image.ContentScale.FILL_BOUNDS)
               .setColorFilter(Image.ColorFilter.TINT)
               .setImageData(logo)
               .build()
           )
           .build()
        )
      ...
      }
    
      private fun resToBitmap(res: Int, width: Int, height: Int): Bitmap {
        val drawable = context?.getDrawable(res)
    
        return drawableToBitmap(drawable!!, width, height)
      }
    
      private fun drawableToBitmap(d: Drawable, width: Int, height: Int): Bitmap {
        val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
    
        val canvas = Canvas(bitmap)
        val left = 0
        val top = 0
        d.setBounds(left, top, canvas.width, canvas.height)
        d.draw(canvas)
    
        return bitmap
     }
     ```
    
    For example:
    
    ![Change the image in the header](/docs/automotive/unbundled_apps/appcards/images/ceramic-06.png)
    
    **Figure 3.** Change the name to the header.
    
  2. Repeat this process to add an image to any component that supports it.

Step 7: Add more interactions

  1. To create a progress bar with controls:

     private var progressOn = false
    
     private var progressTimer: Timer? = null
    
     private var progressCounter = 0
    
     override fun onAppCardRemoved(id: String) {
       when (id) {
         APPCARD_ID -> {
           clickCounter = 0
           progressCounter = 0
           progressTimer?.cancel()
         }
       }
     }
    
     private fun createAppCard(appCardContext: AppCardContext): ImageAppCard {
       val buttonImageSize = appCardContext.imageAppCardContext.getMaxImageSize(Button::class.java)
       val progressPlayPauseImage = resToBitmap(
         if (progressOn) {
           android.R.drawable.ic_media_pause
         } else {
           android.R.drawable.ic_media_play
         },
         buttonImageSize.width,
         buttonImageSize.height
       )
       ...
         .setProgressBar(
           createProgressBar()
         )
         .addButton(
           Button.newBuilder(
             "progressButton",
             Button.ButtonType.NO_BACKGROUND,
             object : OnClickListener {
               override fun onClick() {
                 progressOn = !progressOn
                 if (progressOn) {
                   progressTimer = Timer()
                   progressTimer?.scheduleAtFixedRate(object : TimerTask() {
                     override fun run() {
                     progressCounter++
                     if (progressCounter > 60) progressCounter = 0
                     sendAppCardComponentUpdate(APPCARD_ID, createProgressBar())
                   }
                 }, SECONDS_TO_MS, SECONDS_TO_MS)
               } else {
                 progressTimer?.cancel()
                 progressTimer = null
               }
               sendAppCardUpdate(createAppCard(appCardContext))
             }
           }
         )
           .setImage(
             Image.newBuilder("buttonImage")
               .setContentScale(Image.ContentScale.FILL_BOUNDS)
               .setColorFilter(Image.ColorFilter.TINT)
               .setImageData(progressPlayPauseImage)
               .build()
           )
           .build()
       )
     ...
     }
    
     private fun createProgressBar(): ProgressBar {
       return ProgressBar.newBuilder(PROGRESS_BAR_ID, 0, 60)
         .setProgress(progressCounter)
         .build()
     }
    
     companion object {
       ...
       private const val PROGRESS_BAR_ID = "progress"
       private const val SECONDS_TO_MS = 1000L
     }
    

We added a Play or Pause button to the progress bar that can be updated every second. Due to size constraints, see setText on the clicker button added to reflect click count, .setText("$clickCounter")

Figure 4. Add a button.

Figure 5. Rendered button.

Step 8: Launch the activity

  1. Provided your app complies with background-starts#exceptions, you can launch an activity from the button's onClickListener.

    class SampleRoutingActivity : AppCompatActivity() {
      override fun onStart() {
        super.onStart()
        val intent = Intent(ACTION_LOCATION_SOURCE_SETTINGS).apply {
          setFlags(FLAG_ACTIVITY_CLEAR_TOP)
        }
        startActivity(intent)
        finish()
      }
    }
    
  2. To start the activity, add a button in the AppCard. If none exists, add a routing activity to your app:

    override fun onStart() {
    super.onStart()
    val intent = Intent(ACTION_LOCATION_SOURCE_SETTINGS).apply {
      setFlags(FLAG_ACTIVITY_CLEAR_TOP)
    }
    startActivity(intent)
    finish()
     }
    }
    

    When the class is called, the user is redirected to Location settings. setFlags(FLAG_ACTIVITY_CLEAR_TOP) is applied to ensure the user can't go back to the original activity.

  3. Add a button in the AppCard to start the activity:

     private fun createAppCard(appCardContext: AppCardContext): ImageAppCard {
       val locationImage = resToBitmap(
         android.R.drawable.ic_menu_call,
         buttonImageSize.width,
         buttonImageSize.height
       )
       ...
         .addButton(
           Button.newBuilder(
            "activityButton",
            Button.ButtonType.SECONDARY,
            object : OnClickListener {
              override fun onClick() {
                // no-op
              }
            }
           )
            .setImage(
              Image.newBuilder("locationButtonImage")
                .setContentScale(Image.ContentScale.FILL_BOUNDS)
                .setColorFilter(Image.ColorFilter.TINT)
                .setImageData(locationImage)
                .build()
            )
            .setIntent(
              RoutingActivityIntent
                .newBuilder("com.example.appcard.sample.SampleRoutingActivity")
                .build()
            )
            .build()
          )
        ...
    }
    

Due to space constraints, the clicker button was removed in the AppCard host. The AppCard appears as follows:

Button removed

Figure 6. AppCard without clicker button.