Skip to content

Commit

Permalink
Merge branch '15-extend-documentation-for-custom-drawer' into 'master'
Browse files Browse the repository at this point in the history
Resolve "Extend documentation for custom drawer"

Closes #15

See merge request pace/mobile/android/pace-cloud-sdk!33
  • Loading branch information
Martin Dinh committed Jan 20, 2021
2 parents 70e8a25 + 49b7494 commit 6905c6c
Show file tree
Hide file tree
Showing 6 changed files with 292 additions and 40 deletions.
107 changes: 100 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ This framework combines multipe functionalities provided by PACE i.e. authorizin
+ [AppActivity](#appactivity)
+ [AppWebView](#appwebview)
+ [AppDrawer](#appdrawer)
+ [Default AppDrawer](#default-appdrawer)
+ [Custom AppDrawer](#custom-appdrawer)
+ [Deep Linking](#deep-linking)
+ [Native login](#native-login)
+ [Removal of Apps](#removal-of-apps)
Expand Down Expand Up @@ -258,8 +260,8 @@ A new authorization will be required afterwards.
## AppKit
### Main features
* Get apps at the current location or by URL
* Shows an `AppDrawer` for each app
* Opens the app in the `AppActivity` (recommended) or `AppWebView`
* Shows an [AppDrawer](#appdrawer) for each app
* Opens the app in the [AppActivity](#appactivity) (recommended) or [AppWebView](#appwebview)
* Checks if there is an app for the given POI ID at the current location

### Setup
Expand Down Expand Up @@ -353,7 +355,8 @@ The *AppKit* contains a default `Activity` which can be used to display an app.
Moreover the *AppKit* contains a default `AppWebView`. To display an app in this WebView you have to call `AppWebView.loadApp(parent: Fragment, url: String)`. The `AppWebView`s parent needs to be a fragment as that's the needed context for the integrated `Android Biometric API`.

### AppDrawer
The `AppDrawer` is an expandable button that can be used to display an app. It shows an icon in collapsed state and additionally a title and subtitle in expanded state. By default it will be opened in expanded mode. To fade in the button with an animation, use `appDrawer.show()`. The AppDrawer will be removed if the app is not returned on the next App check again.
#### Default AppDrawer
The default `AppDrawer` is an expandable button that can be used to display an app. It shows an icon in collapsed state and additionally a title and subtitle in expanded state. By default it will be opened in expanded mode. To fade in the button with an animation, use `appDrawer.show()`. The `AppDrawer` will be removed if the app is not returned on the next App check again.

**_Note:_** You need to call `AppKit.requestLocalApps(...)` periodically to make sure that Apps get closed when they are not longer available. This can be done when the app comes into the foreground or the location changes.

Expand All @@ -371,7 +374,7 @@ AppKit.openApps(context, apps, parentLayout, callback = object : AppCallbackImpl
```

The *AppKit* will now create an `AppDrawer` for each app in `apps` and add it to the given `parentLayout`.
Clicking on the button opens the `AppActivity` with the app in the `AppWebView`.
Clicking on the button opens the [AppActivity](#appactivity) with the app in the [AppWebView](#appwebview).

##### Static example
```xml
Expand All @@ -398,11 +401,101 @@ fun showAppDrawer(app: App) {
}
```

#### Custom AppDrawer
If you don't want to use the [default AppDrawer](#default-appdrawer), you can also create your own app drawer/button. To create a custom app drawer/button you need the `App` object that you can request from the *AppKit* using the `AppKit.requestLocalApps()`, `AppKit.requestApps()` or `AppKit.fetchAppsByUrl(...)` methods. All texts are returned in the system language, if available. The app object consists of the following properties:
```kotlin
name: String // e.g. "PACE Connected Fueling"
shortName: String // e.g. "Connected Fueling"
description: String? // e.g. "Pay at the pump"
url: String // App URL
logo: Bitmap? // App logo e.g. from the gas station
iconBackgroundColor: String? // App icon/logo background color e.g. #57C2E4
textBackgroundColor: String? // App background color e.g. #222424
textColor: String? // App text color e.g. #121414
display: String? // Not relevant
gasStationId: String? // Referenced gas station ID, if available
```

You can now display this data in your own views, e.g. your app drawer/button consists of a **title**, **description** and **icon** as in the following XML layout:
```xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/custom_app_button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
android:clickable="true"
android:focusable="true"
android:foreground="?attr/selectableItemBackground"
android:padding="16dp"
tools:background="@android:color/black">

<TextView
android:id="@+id/name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="10dp"
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
android:textColor="@android:color/white"
app:layout_constraintEnd_toStartOf="@id/icon"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="PACE Connected Fueling" />

<TextView
android:id="@+id/description"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:layout_marginEnd="10dp"
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
android:textColor="@android:color/white"
app:layout_constraintEnd_toStartOf="@id/icon"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/name"
tools:text="Pay at the pump" />

<ImageView
android:id="@+id/icon"
android:layout_width="64dp"
android:layout_height="0dp"
android:src="@drawable/ic_default"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>
```
The texts and the icon of the above views can now be set programmatically. When clicking the app drawer/button the app is opened in the [AppActivity](#appactivity) via `AppKit.openAppActivity(...)`:
```kotlin
AppKit.requestLocalApps { app ->
name.text = app.name
description.text = app.description
app.textColor?.let {
val textColor = Color.parseColor(it) // Parses hex color string to color int
name.setTextColor(textColor)
description.setTextColor(textColor)
}

app.logo?.let { icon.setImageBitmap(it) }
app.iconBackgroundColor?.let { icon.setBackgroundColor(Color.parseColor(it)) }
app.textBackgroundColor?.let { itemView.setBackgroundColor(Color.parseColor(it)) }

custom_app_button.setOnClickListener {
AppKit.openAppActivity(context, app, autoClose = false, callback = yourAppCallback)
}
}
```
**Note**: For a more detailed example, where the apps are displayed in a `RecyclerView`, see the `PACECloudSDK` example app.

### Deep Linking
Some of our services (e.g. `PayPal`) do not open the URL in the WebView, but in a Chrome Custom Tab within the app, due to security reasons. After completion of the process the user is redirected back to the WebView via deep linking. In order to set the redirect URL correctly and to ensure that the client app intercepts the deep link, the following requirements must be met:

* Set `clientId` in *PACECloudSDK's* configuration during the [setup](#setup), because it is needed for the redirect URL
* Specify the `AppActivity` as deep link intent filter in your app manifest. **`pace.${clientId}` (same `clientId` as passed in the configuration) must be passed to `android:scheme`:**
* Specify the [AppActivity](#appactivity) as deep link intent filter in your app manifest. **`pace.${clientId}` (same `clientId` as passed in the configuration) must be passed to `android:scheme`:**
* If the scheme is not set, the *AppKit* calls the `onCustomSchemeError(context: Context?, scheme: String)` callback

```xml
Expand Down Expand Up @@ -460,6 +553,6 @@ AppKit.INSTANCE.openAppActivity(context, url, true, false, new AppCallbackImpl()
```

### Removal of Apps
In case you want to remove the `AppActivity`, simply call `AppKit.closeAppActivity()`.
In case you want to remove the [AppActivity](#appactivity), simply call `AppKit.closeAppActivity()`.

If you want to remove all `AppDrawer`s *and* the `AppActivity` (only if it was started with `autoClose = true`), you can call the `AppKit.closeApps(buttonContainer: ConstraintLayout)` method and pass your `ConstraintLayout` where you've added the `AppDrawer`s to (see [AppDrawer](#appdrawer)).
If you want to remove all [AppDrawers](#appdrawer) *and* the [AppActivity](#appactivity) (only if it was started with `autoClose = true`), you can call the `AppKit.closeApps(buttonContainer: ConstraintLayout)` method and pass your `ConstraintLayout` where you've added the `AppDrawer`s to (see [AppDrawer](#appdrawer)).
4 changes: 4 additions & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ android {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}

kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8.toString()
}
}

dependencies {
Expand Down
51 changes: 51 additions & 0 deletions app/src/main/java/cloud/pace/sdk/app/AppListAdapter.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package cloud.pace.sdk.app

import android.graphics.Color
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import cloud.pace.sdk.appkit.model.App
import kotlinx.android.synthetic.main.app_item.view.*

class AppListAdapter(private val onItemClick: (App) -> Unit) : RecyclerView.Adapter<AppListAdapter.AppViewHolder>() {

var entries: List<App> = emptyList()
set(value) {
field = value
notifyDataSetChanged()
}

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AppViewHolder {
return AppViewHolder((LayoutInflater.from(parent.context).inflate(R.layout.app_item, parent, false)))
}

override fun onBindViewHolder(holder: AppViewHolder, position: Int) {
holder.bind(entries[position])
}

override fun getItemCount() = entries.size

inner class AppViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {

private val name = itemView.name
private val description = itemView.description
private val icon = itemView.icon

fun bind(app: App) {
name.text = app.name
description.text = app.description
app.textColor?.let {
val textColor = Color.parseColor(it)
name.setTextColor(textColor)
description.setTextColor(textColor)
}

app.logo?.let { icon.setImageBitmap(it) }
app.iconBackgroundColor?.let { icon.setBackgroundColor(Color.parseColor(it)) }
app.textBackgroundColor?.let { itemView.setBackgroundColor(Color.parseColor(it)) }

itemView.setOnClickListener { onItemClick(app) }
}
}
}
87 changes: 55 additions & 32 deletions app/src/main/java/cloud/pace/sdk/app/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.isVisible
import cloud.pace.sdk.PACECloudSDK
import cloud.pace.sdk.appkit.AppKit
import cloud.pace.sdk.appkit.communication.AppCallbackImpl
Expand All @@ -35,6 +36,33 @@ class MainActivity : AppCompatActivity() {
handleIntent(it.data)
}
}
private val defaultAppCallback = object : AppCallbackImpl() {
override fun onOpen(app: App?) {
appUrl = app?.url
Toast.makeText(this@MainActivity, "Gas station ID = ${app?.gasStationId}", Toast.LENGTH_SHORT).show()
}

override fun onTokenInvalid(onResult: (String) -> Unit) {
IDKit.refreshToken { response ->
(response as? Success)?.result?.let { token -> onResult(token) } ?: run {
radioButtonId = R.id.radio_pending_intents
authorize(appUrl)
}
}
}

override fun onCustomSchemeError(context: Context?, scheme: String) {
context ?: return
AlertDialog.Builder(context)
.setTitle("Payment method not available")
.setMessage("Sorry, this payment method is not supported by this app.")
.setNeutralButton("Close") { dialog, _ ->
dialog.dismiss()
}
.create()
.show()
}
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Expand Down Expand Up @@ -97,11 +125,6 @@ class MainActivity : AppCompatActivity() {
}
}

reset_session.setOnClickListener {
IDKit.resetSession()
info_label.text = "Session reset successful"
}

discover_configuration.setOnClickListener {
// TODO: Replace with your issuerUri
IDKit.discoverConfiguration("YOUR_ISSUER_URI") {
Expand All @@ -117,6 +140,32 @@ class MainActivity : AppCompatActivity() {
}
}
}

reset_session.setOnClickListener {
IDKit.resetSession()
info_label.text = "Session reset successful"
}

val appListAdapter = AppListAdapter {
AppKit.openAppActivity(this, it, autoClose = false, callback = defaultAppCallback)
}
app_list.adapter = appListAdapter
show_app_list.setOnClickListener { button ->
button.isEnabled = false
AppKit.requestLocalApps {
when (it) {
is Success -> {
appListAdapter.entries = it.result
empty_view.isVisible = it.result.isEmpty()
}
is Failure -> {
appListAdapter.entries = emptyList()
empty_view.visibility = View.VISIBLE
}
}
button.isEnabled = true
}
}
}

override fun onStart() {
Expand Down Expand Up @@ -191,33 +240,7 @@ class MainActivity : AppCompatActivity() {
if (lastLocation == null || lastLocation.distanceTo(it) > APP_DISTANCE_THRESHOLD) {
AppKit.requestLocalApps { completion ->
if (completion is Success) {
AppKit.openApps(this, completion.result, root_layout, callback = object : AppCallbackImpl() {
override fun onOpen(app: App?) {
appUrl = app?.url
Toast.makeText(this@MainActivity, "Gas station ID = ${app?.gasStationId}", Toast.LENGTH_SHORT).show()
}

override fun onTokenInvalid(onResult: (String) -> Unit) {
IDKit.refreshToken { response ->
(response as? Success)?.result?.let { token -> onResult(token) } ?: run {
radioButtonId = R.id.radio_pending_intents
authorize(appUrl)
}
}
}

override fun onCustomSchemeError(context: Context?, scheme: String) {
context ?: return
AlertDialog.Builder(context)
.setTitle("Payment method not available")
.setMessage("Sorry, this payment method is not supported by this app.")
.setNeutralButton("Close") { dialog, _ ->
dialog.dismiss()
}
.create()
.show()
}
})
AppKit.openApps(this, completion.result, root_layout, bottomMargin = 100f, callback = defaultAppCallback)
}
}

Expand Down
34 changes: 33 additions & 1 deletion app/src/main/res/layout/activity_main.xml
Original file line number Diff line number Diff line change
Expand Up @@ -129,11 +129,43 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="40dp"
android:background="@android:color/holo_red_dark"
android:background="@android:color/black"
android:text="Reset session"
android:textColor="@android:color/white"
app:layout_constraintTop_toBottomOf="@id/registration_endpoint" />

<Button
android:id="@+id/show_app_list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="40dp"
android:background="@android:color/holo_red_light"
android:text="Show App list"
android:textColor="@android:color/white"
app:layout_constraintTop_toBottomOf="@id/reset_session" />

<androidx.recyclerview.widget.RecyclerView
android:id="@+id/app_list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:nestedScrollingEnabled="false"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintTop_toBottomOf="@id/show_app_list"
tools:listitem="@layout/app_item" />

<TextView
android:id="@+id/empty_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="20dp"
android:text="No apps available at the current location"
android:textAlignment="center"
android:textColor="@android:color/black"
android:textSize="20sp"
android:visibility="gone"
app:layout_constraintTop_toBottomOf="@id/show_app_list" />

</androidx.constraintlayout.widget.ConstraintLayout>

</ScrollView>
Expand Down
Loading

0 comments on commit 6905c6c

Please sign in to comment.