Skip to content

Commit 6905c6c

Browse files
author
Martin Dinh
committed
Merge branch '15-extend-documentation-for-custom-drawer' into 'master'
Resolve "Extend documentation for custom drawer" Closes #15 See merge request pace/mobile/android/pace-cloud-sdk!33
2 parents 70e8a25 + 49b7494 commit 6905c6c

File tree

6 files changed

+292
-40
lines changed

6 files changed

+292
-40
lines changed

README.md

Lines changed: 100 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ This framework combines multipe functionalities provided by PACE i.e. authorizin
2525
+ [AppActivity](#appactivity)
2626
+ [AppWebView](#appwebview)
2727
+ [AppDrawer](#appdrawer)
28+
+ [Default AppDrawer](#default-appdrawer)
29+
+ [Custom AppDrawer](#custom-appdrawer)
2830
+ [Deep Linking](#deep-linking)
2931
+ [Native login](#native-login)
3032
+ [Removal of Apps](#removal-of-apps)
@@ -258,8 +260,8 @@ A new authorization will be required afterwards.
258260
## AppKit
259261
### Main features
260262
* Get apps at the current location or by URL
261-
* Shows an `AppDrawer` for each app
262-
* Opens the app in the `AppActivity` (recommended) or `AppWebView`
263+
* Shows an [AppDrawer](#appdrawer) for each app
264+
* Opens the app in the [AppActivity](#appactivity) (recommended) or [AppWebView](#appwebview)
263265
* Checks if there is an app for the given POI ID at the current location
264266

265267
### Setup
@@ -353,7 +355,8 @@ The *AppKit* contains a default `Activity` which can be used to display an app.
353355
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`.
354356

355357
### AppDrawer
356-
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.
358+
#### Default AppDrawer
359+
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.
357360

358361
**_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.
359362

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

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

376379
##### Static example
377380
```xml
@@ -398,11 +401,101 @@ fun showAppDrawer(app: App) {
398401
}
399402
```
400403

404+
#### Custom AppDrawer
405+
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:
406+
```kotlin
407+
name: String // e.g. "PACE Connected Fueling"
408+
shortName: String // e.g. "Connected Fueling"
409+
description: String? // e.g. "Pay at the pump"
410+
url: String // App URL
411+
logo: Bitmap? // App logo e.g. from the gas station
412+
iconBackgroundColor: String? // App icon/logo background color e.g. #57C2E4
413+
textBackgroundColor: String? // App background color e.g. #222424
414+
textColor: String? // App text color e.g. #121414
415+
display: String? // Not relevant
416+
gasStationId: String? // Referenced gas station ID, if available
417+
```
418+
419+
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:
420+
```xml
421+
<?xml version="1.0" encoding="utf-8"?>
422+
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
423+
xmlns:app="http://schemas.android.com/apk/res-auto"
424+
xmlns:tools="http://schemas.android.com/tools"
425+
android:id="@+id/custom_app_button"
426+
android:layout_width="match_parent"
427+
android:layout_height="wrap_content"
428+
android:layout_marginTop="8dp"
429+
android:layout_marginBottom="8dp"
430+
android:clickable="true"
431+
android:focusable="true"
432+
android:foreground="?attr/selectableItemBackground"
433+
android:padding="16dp"
434+
tools:background="@android:color/black">
435+
436+
<TextView
437+
android:id="@+id/name"
438+
android:layout_width="0dp"
439+
android:layout_height="wrap_content"
440+
android:layout_marginEnd="10dp"
441+
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
442+
android:textColor="@android:color/white"
443+
app:layout_constraintEnd_toStartOf="@id/icon"
444+
app:layout_constraintStart_toStartOf="parent"
445+
app:layout_constraintTop_toTopOf="parent"
446+
tools:text="PACE Connected Fueling" />
447+
448+
<TextView
449+
android:id="@+id/description"
450+
android:layout_width="0dp"
451+
android:layout_height="wrap_content"
452+
android:layout_marginTop="5dp"
453+
android:layout_marginEnd="10dp"
454+
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
455+
android:textColor="@android:color/white"
456+
app:layout_constraintEnd_toStartOf="@id/icon"
457+
app:layout_constraintStart_toStartOf="parent"
458+
app:layout_constraintTop_toBottomOf="@id/name"
459+
tools:text="Pay at the pump" />
460+
461+
<ImageView
462+
android:id="@+id/icon"
463+
android:layout_width="64dp"
464+
android:layout_height="0dp"
465+
android:src="@drawable/ic_default"
466+
app:layout_constraintBottom_toBottomOf="parent"
467+
app:layout_constraintEnd_toEndOf="parent"
468+
app:layout_constraintTop_toTopOf="parent" />
469+
470+
</androidx.constraintlayout.widget.ConstraintLayout>
471+
```
472+
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(...)`:
473+
```kotlin
474+
AppKit.requestLocalApps { app ->
475+
name.text = app.name
476+
description.text = app.description
477+
app.textColor?.let {
478+
val textColor = Color.parseColor(it) // Parses hex color string to color int
479+
name.setTextColor(textColor)
480+
description.setTextColor(textColor)
481+
}
482+
483+
app.logo?.let { icon.setImageBitmap(it) }
484+
app.iconBackgroundColor?.let { icon.setBackgroundColor(Color.parseColor(it)) }
485+
app.textBackgroundColor?.let { itemView.setBackgroundColor(Color.parseColor(it)) }
486+
487+
custom_app_button.setOnClickListener {
488+
AppKit.openAppActivity(context, app, autoClose = false, callback = yourAppCallback)
489+
}
490+
}
491+
```
492+
**Note**: For a more detailed example, where the apps are displayed in a `RecyclerView`, see the `PACECloudSDK` example app.
493+
401494
### Deep Linking
402495
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:
403496

404497
* Set `clientId` in *PACECloudSDK's* configuration during the [setup](#setup), because it is needed for the redirect URL
405-
* 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`:**
498+
* 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`:**
406499
* If the scheme is not set, the *AppKit* calls the `onCustomSchemeError(context: Context?, scheme: String)` callback
407500

408501
```xml
@@ -460,6 +553,6 @@ AppKit.INSTANCE.openAppActivity(context, url, true, false, new AppCallbackImpl()
460553
```
461554

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

465-
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)).
558+
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)).

app/build.gradle

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@ android {
2929
sourceCompatibility JavaVersion.VERSION_1_8
3030
targetCompatibility JavaVersion.VERSION_1_8
3131
}
32+
33+
kotlinOptions {
34+
jvmTarget = JavaVersion.VERSION_1_8.toString()
35+
}
3236
}
3337

3438
dependencies {
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package cloud.pace.sdk.app
2+
3+
import android.graphics.Color
4+
import android.view.LayoutInflater
5+
import android.view.View
6+
import android.view.ViewGroup
7+
import androidx.recyclerview.widget.RecyclerView
8+
import cloud.pace.sdk.appkit.model.App
9+
import kotlinx.android.synthetic.main.app_item.view.*
10+
11+
class AppListAdapter(private val onItemClick: (App) -> Unit) : RecyclerView.Adapter<AppListAdapter.AppViewHolder>() {
12+
13+
var entries: List<App> = emptyList()
14+
set(value) {
15+
field = value
16+
notifyDataSetChanged()
17+
}
18+
19+
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AppViewHolder {
20+
return AppViewHolder((LayoutInflater.from(parent.context).inflate(R.layout.app_item, parent, false)))
21+
}
22+
23+
override fun onBindViewHolder(holder: AppViewHolder, position: Int) {
24+
holder.bind(entries[position])
25+
}
26+
27+
override fun getItemCount() = entries.size
28+
29+
inner class AppViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
30+
31+
private val name = itemView.name
32+
private val description = itemView.description
33+
private val icon = itemView.icon
34+
35+
fun bind(app: App) {
36+
name.text = app.name
37+
description.text = app.description
38+
app.textColor?.let {
39+
val textColor = Color.parseColor(it)
40+
name.setTextColor(textColor)
41+
description.setTextColor(textColor)
42+
}
43+
44+
app.logo?.let { icon.setImageBitmap(it) }
45+
app.iconBackgroundColor?.let { icon.setBackgroundColor(Color.parseColor(it)) }
46+
app.textBackgroundColor?.let { itemView.setBackgroundColor(Color.parseColor(it)) }
47+
48+
itemView.setOnClickListener { onItemClick(app) }
49+
}
50+
}
51+
}

app/src/main/java/cloud/pace/sdk/app/MainActivity.kt

Lines changed: 55 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import android.widget.Toast
1313
import androidx.activity.result.contract.ActivityResultContracts
1414
import androidx.appcompat.app.AlertDialog
1515
import androidx.appcompat.app.AppCompatActivity
16+
import androidx.core.view.isVisible
1617
import cloud.pace.sdk.PACECloudSDK
1718
import cloud.pace.sdk.appkit.AppKit
1819
import cloud.pace.sdk.appkit.communication.AppCallbackImpl
@@ -35,6 +36,33 @@ class MainActivity : AppCompatActivity() {
3536
handleIntent(it.data)
3637
}
3738
}
39+
private val defaultAppCallback = object : AppCallbackImpl() {
40+
override fun onOpen(app: App?) {
41+
appUrl = app?.url
42+
Toast.makeText(this@MainActivity, "Gas station ID = ${app?.gasStationId}", Toast.LENGTH_SHORT).show()
43+
}
44+
45+
override fun onTokenInvalid(onResult: (String) -> Unit) {
46+
IDKit.refreshToken { response ->
47+
(response as? Success)?.result?.let { token -> onResult(token) } ?: run {
48+
radioButtonId = R.id.radio_pending_intents
49+
authorize(appUrl)
50+
}
51+
}
52+
}
53+
54+
override fun onCustomSchemeError(context: Context?, scheme: String) {
55+
context ?: return
56+
AlertDialog.Builder(context)
57+
.setTitle("Payment method not available")
58+
.setMessage("Sorry, this payment method is not supported by this app.")
59+
.setNeutralButton("Close") { dialog, _ ->
60+
dialog.dismiss()
61+
}
62+
.create()
63+
.show()
64+
}
65+
}
3866

3967
override fun onCreate(savedInstanceState: Bundle?) {
4068
super.onCreate(savedInstanceState)
@@ -97,11 +125,6 @@ class MainActivity : AppCompatActivity() {
97125
}
98126
}
99127

100-
reset_session.setOnClickListener {
101-
IDKit.resetSession()
102-
info_label.text = "Session reset successful"
103-
}
104-
105128
discover_configuration.setOnClickListener {
106129
// TODO: Replace with your issuerUri
107130
IDKit.discoverConfiguration("YOUR_ISSUER_URI") {
@@ -117,6 +140,32 @@ class MainActivity : AppCompatActivity() {
117140
}
118141
}
119142
}
143+
144+
reset_session.setOnClickListener {
145+
IDKit.resetSession()
146+
info_label.text = "Session reset successful"
147+
}
148+
149+
val appListAdapter = AppListAdapter {
150+
AppKit.openAppActivity(this, it, autoClose = false, callback = defaultAppCallback)
151+
}
152+
app_list.adapter = appListAdapter
153+
show_app_list.setOnClickListener { button ->
154+
button.isEnabled = false
155+
AppKit.requestLocalApps {
156+
when (it) {
157+
is Success -> {
158+
appListAdapter.entries = it.result
159+
empty_view.isVisible = it.result.isEmpty()
160+
}
161+
is Failure -> {
162+
appListAdapter.entries = emptyList()
163+
empty_view.visibility = View.VISIBLE
164+
}
165+
}
166+
button.isEnabled = true
167+
}
168+
}
120169
}
121170

122171
override fun onStart() {
@@ -191,33 +240,7 @@ class MainActivity : AppCompatActivity() {
191240
if (lastLocation == null || lastLocation.distanceTo(it) > APP_DISTANCE_THRESHOLD) {
192241
AppKit.requestLocalApps { completion ->
193242
if (completion is Success) {
194-
AppKit.openApps(this, completion.result, root_layout, callback = object : AppCallbackImpl() {
195-
override fun onOpen(app: App?) {
196-
appUrl = app?.url
197-
Toast.makeText(this@MainActivity, "Gas station ID = ${app?.gasStationId}", Toast.LENGTH_SHORT).show()
198-
}
199-
200-
override fun onTokenInvalid(onResult: (String) -> Unit) {
201-
IDKit.refreshToken { response ->
202-
(response as? Success)?.result?.let { token -> onResult(token) } ?: run {
203-
radioButtonId = R.id.radio_pending_intents
204-
authorize(appUrl)
205-
}
206-
}
207-
}
208-
209-
override fun onCustomSchemeError(context: Context?, scheme: String) {
210-
context ?: return
211-
AlertDialog.Builder(context)
212-
.setTitle("Payment method not available")
213-
.setMessage("Sorry, this payment method is not supported by this app.")
214-
.setNeutralButton("Close") { dialog, _ ->
215-
dialog.dismiss()
216-
}
217-
.create()
218-
.show()
219-
}
220-
})
243+
AppKit.openApps(this, completion.result, root_layout, bottomMargin = 100f, callback = defaultAppCallback)
221244
}
222245
}
223246

app/src/main/res/layout/activity_main.xml

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,11 +129,43 @@
129129
android:layout_width="match_parent"
130130
android:layout_height="wrap_content"
131131
android:layout_marginTop="40dp"
132-
android:background="@android:color/holo_red_dark"
132+
android:background="@android:color/black"
133133
android:text="Reset session"
134134
android:textColor="@android:color/white"
135135
app:layout_constraintTop_toBottomOf="@id/registration_endpoint" />
136136

137+
<Button
138+
android:id="@+id/show_app_list"
139+
android:layout_width="match_parent"
140+
android:layout_height="wrap_content"
141+
android:layout_marginTop="40dp"
142+
android:background="@android:color/holo_red_light"
143+
android:text="Show App list"
144+
android:textColor="@android:color/white"
145+
app:layout_constraintTop_toBottomOf="@id/reset_session" />
146+
147+
<androidx.recyclerview.widget.RecyclerView
148+
android:id="@+id/app_list"
149+
android:layout_width="match_parent"
150+
android:layout_height="wrap_content"
151+
android:layout_marginTop="20dp"
152+
android:nestedScrollingEnabled="false"
153+
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
154+
app:layout_constraintTop_toBottomOf="@id/show_app_list"
155+
tools:listitem="@layout/app_item" />
156+
157+
<TextView
158+
android:id="@+id/empty_view"
159+
android:layout_width="match_parent"
160+
android:layout_height="match_parent"
161+
android:layout_marginTop="20dp"
162+
android:text="No apps available at the current location"
163+
android:textAlignment="center"
164+
android:textColor="@android:color/black"
165+
android:textSize="20sp"
166+
android:visibility="gone"
167+
app:layout_constraintTop_toBottomOf="@id/show_app_list" />
168+
137169
</androidx.constraintlayout.widget.ConstraintLayout>
138170

139171
</ScrollView>

0 commit comments

Comments
 (0)