Skip to content

Commit 662ad9f

Browse files
dipjadipjadipja
authored
Adding resources for dynamic type ahead UI and new screen for type ahead (#8273)
* adding resources for dynamic type ahead ui * adding custom icons * fix typo * addressing comments * adding msft copyright * Adding new screen for dynamic type ahead (#8274) * adding new screen for dynamic type ahead * removing comments * updating sample app to use channel adaptor to fetch dynamic choices * fixing customization support in new type ahead screen * setting content description for buttons --------- Co-authored-by: dipja <dipja@Dipeshs-MacBook-Pro.local> Co-authored-by: dipja <dipja@Dipeshs-MBP.guest.corp.microsoft.com> --------- Co-authored-by: dipja <dipja@Dipeshs-MacBook-Pro.local> Co-authored-by: dipja <dipja@Dipeshs-MBP.guest.corp.microsoft.com>
1 parent 8d2e578 commit 662ad9f

38 files changed

+1480
-8
lines changed

source/android/adaptivecards/build.gradle

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ android {
2626
}
2727
}
2828
}
29+
buildFeatures {
30+
dataBinding true
31+
}
2932
buildTypes {
3033
release {
3134
minifyEnabled false
@@ -186,4 +189,9 @@ dependencies {
186189
implementation 'androidx.core:core-ktx:' + androidxKotlinCoreVersion
187190
implementation 'com.google.android.exoplayer:exoplayer:' + androidExoPlayerVersion
188191
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
192+
implementation 'androidx.recyclerview:recyclerview:' + androidxRecyclerViewVersion
193+
implementation 'androidx.recyclerview:recyclerview-selection:' + androidxRecyclerViewSelectionVersion
194+
implementation 'com.google.android.material:material:' + androidMaterialVersion
195+
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:' + androidxLifecycleVersion
196+
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:' + androidxLifecycleVersion
189197
}

source/android/adaptivecards/src/main/AndroidManifest.xml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44

55
<application android:allowBackup="true" android:label="@string/app_name"
66
android:supportsRtl="true">
7-
7+
<activity
8+
android:name=".renderer.typeaheadsearch.TypeAheadSearchActivity" />
89
</application>
910

1011
</manifest>
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
package io.adaptivecards.renderer
4+
5+
import io.adaptivecards.renderer.typeaheadsearch.*
6+
7+
interface ITypeAheadRenderer {
8+
fun getSearchIconParams(): SearchIconParams
9+
10+
fun getCrossIconParams(): CrossIconParams
11+
12+
fun getTickIconParams(): TickIconParams
13+
14+
fun getBackIconParams(): BackIconParams
15+
16+
fun getStartSearchingStateParams(): StartSearchingStateParams
17+
18+
fun getErrorStateParams(): ErrorStateParams
19+
20+
fun getNoResultStateParams(): NoResultStateParams
21+
22+
fun getScreenTitle(): String
23+
24+
fun getPrimaryColor(): Int
25+
26+
fun getSecondaryColor(): Int
27+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
package io.adaptivecards.renderer
4+
5+
import android.util.Log
6+
import androidx.annotation.MainThread
7+
import androidx.lifecycle.LifecycleOwner
8+
import androidx.lifecycle.MutableLiveData
9+
import androidx.lifecycle.Observer
10+
import java.util.concurrent.atomic.AtomicBoolean
11+
12+
/**
13+
* A lifecycle-aware observable that sends only new updates after subscription, used for events like
14+
* navigation and Snackbar messages.
15+
* <p>
16+
* This avoids a common problem with events: on configuration change (like rotation) an update
17+
* can be emitted if the observer is active. This LiveData only calls the observable if there's an
18+
* explicit call to setValue() or call().
19+
* <p>
20+
* Note that only one observer is going to be notified of changes.
21+
*
22+
* @param <T> data type of data being delivered by single live event
23+
*/
24+
class SingleLiveEvent<T> : MutableLiveData<T?>() {
25+
private val mPending = AtomicBoolean(false)
26+
27+
@MainThread
28+
override fun observe(owner: LifecycleOwner, observer: Observer<in T?>) {
29+
if (hasActiveObservers()) {
30+
Log.w(TAG, "Multiple observers registered but only one will be notified of changes.")
31+
}
32+
33+
// Observe the internal MutableLiveData
34+
super.observe(owner) { t ->
35+
if (mPending.compareAndSet(true, false)) {
36+
observer.onChanged(t)
37+
}
38+
}
39+
}
40+
41+
@MainThread
42+
override fun setValue(t: T?) {
43+
mPending.set(true)
44+
super.setValue(t)
45+
}
46+
47+
/**
48+
* Used for cases where T is Void, to make calls cleaner.
49+
*/
50+
@MainThread
51+
fun call() {
52+
value = null
53+
}
54+
55+
companion object {
56+
private const val TAG = "SingleLiveEvent"
57+
}
58+
}

source/android/adaptivecards/src/main/java/io/adaptivecards/renderer/http/HttpRequestResult.java

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,18 @@
44

55
public class HttpRequestResult<RESULT>
66
{
7-
public HttpRequestResult(Exception excep)
7+
public HttpRequestResult(Exception exception)
88
{
9-
m_excep = excep;
9+
m_exception = exception;
1010
m_success = false;
1111
}
1212

13+
public HttpRequestResult(Exception exception, String errorMessage)
14+
{
15+
this(exception);
16+
m_errorMessage = errorMessage;
17+
}
18+
1319
public HttpRequestResult(RESULT result)
1420
{
1521
m_result = result;
@@ -26,15 +32,20 @@ public RESULT getResult()
2632

2733
public Exception getException()
2834
{
29-
return m_excep;
35+
return m_exception;
3036
}
3137

3238
public boolean isSuccessful()
3339
{
3440
return m_success;
3541
}
3642

37-
private Exception m_excep = null;
43+
public String getErrorMessage() {
44+
return m_errorMessage;
45+
}
46+
47+
private Exception m_exception = null;
3848
private RESULT m_result = null;
3949
private boolean m_success = true;
50+
private String m_errorMessage = null;
4051
}

source/android/adaptivecards/src/main/java/io/adaptivecards/renderer/input/ChoiceSetInputRenderer.java

Lines changed: 138 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,24 @@
22
// Licensed under the MIT License.
33
package io.adaptivecards.renderer.input;
44

5+
import static androidx.appcompat.content.res.AppCompatResources.getDrawable;
6+
7+
import android.app.Activity;
58
import android.content.Context;
9+
import android.content.Intent;
610
import android.content.res.Resources;
11+
12+
import androidx.activity.result.ActivityResultLauncher;
13+
import androidx.activity.result.ActivityResultRegistry;
14+
import androidx.activity.result.contract.ActivityResultContracts;
715
import androidx.annotation.NonNull;
816
import androidx.annotation.Nullable;
917
import androidx.fragment.app.FragmentManager;
1018

19+
import android.graphics.drawable.Drawable;
1120
import android.os.Build;
1221
import android.text.TextUtils;
22+
import android.util.Log;
1323
import android.util.TypedValue;
1424
import android.view.KeyEvent;
1525
import android.view.MotionEvent;
@@ -44,6 +54,7 @@
4454
import io.adaptivecards.renderer.input.customcontrols.ValidatedRadioGroup;
4555
import io.adaptivecards.renderer.input.customcontrols.ValidatedSpinner;
4656
import io.adaptivecards.renderer.input.customcontrols.ValidatedSpinnerLayout;
57+
import io.adaptivecards.renderer.input.customcontrols.ValidatedTextView;
4758
import io.adaptivecards.renderer.inputhandler.AutoCompleteTextViewHandler;
4859
import io.adaptivecards.renderer.inputhandler.CheckBoxSetInputHandler;
4960
import io.adaptivecards.renderer.inputhandler.ComboBoxInputHandler;
@@ -52,7 +63,12 @@
5263
import io.adaptivecards.objectmodel.HostConfig;
5364
import io.adaptivecards.renderer.BaseCardElementRenderer;
5465
import io.adaptivecards.renderer.inputhandler.RadioGroupInputHandler;
66+
import io.adaptivecards.renderer.inputhandler.TypeAheadTextViewHandler;
5567
import io.adaptivecards.renderer.registration.CardRendererRegistration;
68+
import io.adaptivecards.renderer.typeaheadsearch.DynamicTypeAheadService;
69+
import io.adaptivecards.renderer.typeaheadsearch.IChoicesResolver;
70+
import io.adaptivecards.renderer.typeaheadsearch.TypeAheadSearchActivity;
71+
import io.adaptivecards.renderer.typeaheadsearch.TypeAheadSearchLaunchParams;
5672

5773
import java.util.ArrayList;
5874
import java.util.Arrays;
@@ -611,6 +627,123 @@ public void onNothingSelected(AdapterView<?> parent)
611627
}
612628
}
613629

630+
public View renderTypeAheadControl(
631+
RenderedAdaptiveCard renderedCard,
632+
Context context,
633+
ChoiceSetInput choiceSetInput,
634+
IChoicesResolver choicesResolver,
635+
HostConfig hostConfig,
636+
RenderArgs renderArgs)
637+
{
638+
final List<String> titleList = new ArrayList();
639+
ChoiceInputVector choiceInputVector = choiceSetInput.GetChoices();
640+
long size = choiceInputVector.size();
641+
int valueIndex = -1;
642+
String value = choiceSetInput.GetValue();
643+
final List<String> valueList = new ArrayList();
644+
for (int i = 0; i < size; i++)
645+
{
646+
ChoiceInput choiceInput = choiceInputVector.get(i);
647+
titleList.add(choiceInput.GetTitle());
648+
valueList.add(choiceInput.GetTitle());
649+
650+
if (choiceInput.GetValue().equals(value))
651+
{
652+
valueIndex = i;
653+
}
654+
}
655+
656+
boolean usingCustomInputs = isUsingCustomInputs(context);
657+
658+
ValidatedTextView validatedTypeAheadTextView = new ValidatedTextView(context, usingCustomInputs);
659+
660+
Drawable chevronDrawable = getDrawable(context, R.drawable.adaptive_card_ic_chevron_right);
661+
chevronDrawable.setTint(getColor(hostConfig.GetForegroundColor(ContainerStyle.Default, ForegroundColor.Default, false)));
662+
validatedTypeAheadTextView.setCompoundDrawablesWithIntrinsicBounds(null, null, chevronDrawable, null);
663+
validatedTypeAheadTextView.setPaddingRelative(0, 10, 20, 10);
664+
validatedTypeAheadTextView.setEllipsize(TextUtils.TruncateAt.END);
665+
666+
final TypeAheadTextViewHandler typeAheadTextInputHandler = new TypeAheadTextViewHandler(choiceSetInput);
667+
668+
ValidatedInputLayout inputLayout = null;
669+
670+
// if using custom inputs, we don't have to create the surrounding linear layout
671+
boolean needsOuterLayout = (!usingCustomInputs);
672+
if (needsOuterLayout)
673+
{
674+
inputLayout = new ValidatedSpinnerLayout(context,
675+
getColor(hostConfig.GetForegroundColor(ContainerStyle.Default, ForegroundColor.Attention, false)));
676+
inputLayout.setTag(new TagContent(choiceSetInput, typeAheadTextInputHandler));
677+
typeAheadTextInputHandler.setView(inputLayout);
678+
}
679+
else
680+
{
681+
validatedTypeAheadTextView.setTag(new TagContent(choiceSetInput, typeAheadTextInputHandler));
682+
typeAheadTextInputHandler.setView(validatedTypeAheadTextView);
683+
}
684+
renderedCard.registerInputHandler(typeAheadTextInputHandler, renderArgs.getContainerCardId());
685+
686+
validatedTypeAheadTextView.setFocusable(true);
687+
if (valueIndex != -1)
688+
{
689+
validatedTypeAheadTextView.setText(titleList.get(valueIndex));
690+
typeAheadTextInputHandler.setInput(titleList.get(valueIndex), valueList.get(valueIndex));
691+
}
692+
693+
validatedTypeAheadTextView.setOnClickListener(view -> {
694+
ActivityResultRegistry registry = CardRendererRegistration.getInstance().getActivityResultRegistry();
695+
if (registry != null) {
696+
ActivityResultLauncher<Intent> launcher = registry.register("adaptive-card-dynamic-type-ahead",
697+
new ActivityResultContracts.StartActivityForResult(),
698+
result -> {
699+
if (result.getResultCode() == Activity.RESULT_OK
700+
&& result.getData() != null
701+
&& result.getData().getExtras() != null
702+
&& result.getData().getExtras().getString("typeAheadSearchSelectedKey") != null) {
703+
String selectedTitle = result.getData().getExtras().getString("typeAheadSearchSelectedKey");
704+
String selectedValue = result.getData().getExtras().getString("typeAheadSearchSelectedValue");
705+
706+
validatedTypeAheadTextView.setText(selectedTitle);
707+
typeAheadTextInputHandler.setInput(selectedTitle, selectedValue);
708+
CardRendererRegistration.getInstance().notifyInputChange(typeAheadTextInputHandler.getId(), typeAheadTextInputHandler.getInput());
709+
Log.d("SelectedChoice", selectedTitle);
710+
}
711+
DynamicTypeAheadService.INSTANCE.removeIChoicesResolver();
712+
}
713+
);
714+
715+
Intent intent = new Intent(context, TypeAheadSearchActivity.class);
716+
TypeAheadSearchLaunchParams launchParams = new TypeAheadSearchLaunchParams(
717+
typeAheadTextInputHandler.getInputTitle(),
718+
choiceSetInput.GetChoicesData().GetChoicesDataType(),
719+
choiceSetInput.GetChoicesData().GetDataset(),
720+
titleList,
721+
valueList,
722+
getColor(hostConfig.GetBackgroundColor(ContainerStyle.Default)),
723+
getColor(hostConfig.GetForegroundColor(ContainerStyle.Default, ForegroundColor.Default, false)));
724+
intent.putExtra("launchParams", launchParams);
725+
726+
DynamicTypeAheadService.INSTANCE.setIChoicesResolver(choicesResolver);
727+
launcher.launch(intent);
728+
}
729+
else {
730+
renderedCard.addWarning(
731+
new AdaptiveWarning(AdaptiveWarning.INTERACTIVITY_DISALLOWED,
732+
"Interactivity is not allowed. ActivityResultRegistry is null.")
733+
);
734+
}
735+
});
736+
737+
if (needsOuterLayout)
738+
{
739+
inputLayout.addView(validatedTypeAheadTextView);
740+
return inputLayout;
741+
}
742+
else
743+
{
744+
return validatedTypeAheadTextView;
745+
}
746+
}
614747

615748
@Override
616749
public View render(
@@ -632,7 +765,11 @@ public View render(
632765
ChoiceSetInput choiceSetInput = Util.castTo(baseCardElement, ChoiceSetInput.class);
633766

634767
View inputView = null;
635-
if (choiceSetInput.GetIsMultiSelect())
768+
if (choiceSetInput.GetChoicesData() != null && !choiceSetInput.GetChoicesData().GetChoicesDataType().isEmpty()) {
769+
// Create dynamic type ahead control
770+
inputView = renderTypeAheadControl(renderedCard, context, choiceSetInput, channelAdaptor.getChoicesResolver(), hostConfig, renderArgs);
771+
}
772+
else if (choiceSetInput.GetIsMultiSelect())
636773
{
637774
// Create multi-select checkbox
638775
inputView = renderCheckBoxSet(renderedCard, context, choiceSetInput, hostConfig, renderArgs);

0 commit comments

Comments
 (0)