1 /*
2  * Copyright (C) 2021 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.android.car.cluster.view;
17 
18 import android.car.cluster.navigation.NavigationState.Cue;
19 import android.car.cluster.navigation.NavigationState.Cue.CueElement;
20 import android.car.cluster.navigation.NavigationState.ImageReference;
21 import android.content.Context;
22 import android.graphics.Bitmap;
23 import android.graphics.drawable.BitmapDrawable;
24 import android.os.Handler;
25 import android.text.SpannableStringBuilder;
26 import android.text.style.ImageSpan;
27 import android.util.AttributeSet;
28 import android.util.Log;
29 import android.widget.TextView;
30 
31 import com.android.car.cluster.osdouble.R;
32 
33 import java.util.Collections;
34 import java.util.List;
35 import java.util.Map;
36 import java.util.Objects;
37 import java.util.concurrent.CompletableFuture;
38 import java.util.stream.Collectors;
39 
40 /**
41  * View component that displays the Cue information on the instrument cluster display
42  */
43 public class CueView extends TextView {
44     private static final String TAG = "Cluster.CueView";
45 
46     private String mImageSpanText;
47     private CompletableFuture<?> mFuture;
48     private Handler mHandler = new Handler();
49     private Cue mContent;
50 
CueView(Context context)51     public CueView(Context context) {
52         super(context);
53         init(context);
54     }
55 
CueView(Context context, AttributeSet attrs)56     public CueView(Context context, AttributeSet attrs) {
57         super(context, attrs);
58         init(context);
59     }
60 
CueView(Context context, AttributeSet attrs, int defStyle)61     public CueView(Context context, AttributeSet attrs, int defStyle) {
62         super(context, attrs, defStyle);
63         init(context);
64     }
65 
init(Context context)66     private void init(Context context) {
67         mImageSpanText = context.getString(R.string.span_image);
68     }
69 
70     /**
71      * Set the given {@link Cue} to the View.
72      * @param cue {@link Cue} to set in the view
73      * @param imageResolver {@link ImageResolver} to fetch the {@link Bitmap}
74      * @param alpha the opacity of the image
75      */
setCue(Cue cue, ImageResolver imageResolver, float alpha)76     public void setCue(Cue cue, ImageResolver imageResolver, float alpha) {
77         if (cue == null) {
78             setText(null);
79             return;
80         }
81 
82         if (mFuture != null && !Objects.equals(cue, mContent)) {
83             mFuture.cancel(true);
84         }
85 
86         List<ImageReference> imageReferences = cue.getElementsList().stream()
87                 .filter(element -> element.hasImage())
88                 .map(element -> element.getImage())
89                 .collect(Collectors.toList());
90         mFuture = imageResolver
91                 .getBitmaps(imageReferences, 0, getLineHeight())
92                 .thenAccept(bitmaps -> {
93                     mHandler.post(() -> update(cue, bitmaps, alpha));
94                     mFuture = null;
95                 })
96                 .exceptionally(ex -> {
97                     if (Log.isLoggable(TAG, Log.DEBUG)) {
98                         Log.d(TAG, "Unable to fetch images for cue: " + cue);
99                     }
100                     mHandler.post(
101                             () -> update(cue, Collections.emptyMap(), alpha));
102                     return null;
103                 });
104         mContent = cue;
105     }
106 
update(Cue cue, Map<ImageReference, Bitmap> bitmaps, float alpha)107     private void update(Cue cue, Map<ImageReference, Bitmap> bitmaps, float alpha) {
108         SpannableStringBuilder builder = new SpannableStringBuilder();
109 
110         for (CueElement element : cue.getElementsList()) {
111             if (element.hasImage()) {
112                 Bitmap bitmap = bitmaps.get(element.getImage());
113                 if (bitmap != null) {
114                     String imageText = element.getText().isEmpty() ? mImageSpanText :
115                             element.getText();
116                     appendImage(builder, imageText, bitmap, alpha);
117                 }
118             } else if (!element.getText().isEmpty()) {
119                 builder.append(element.getText());
120             }
121         }
122 
123         setText(builder);
124         setAlpha(alpha);
125     }
126 
appendImage(SpannableStringBuilder builder, String text, Bitmap bitmap, float alpha)127     private void appendImage(SpannableStringBuilder builder, String text, Bitmap bitmap,
128             float alpha) {
129         int start = builder.length();
130         int end = start + text.length();
131         builder.append(text);
132         BitmapDrawable drawable = new BitmapDrawable(getResources(), bitmap);
133         drawable.setBounds(0, 0, bitmap.getWidth(), bitmap.getHeight());
134         drawable.setAlpha((int) (alpha * 255));
135         builder.setSpan(new ImageSpan(drawable), start, end, 0);
136     }
137 }
138