1 /*
2  * Copyright 2019 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 
17 package com.android.car.apps.common.imaging;
18 
19 
20 import static com.android.internal.util.Preconditions.checkNotNull;
21 
22 import android.annotation.NonNull;
23 import android.annotation.Nullable;
24 import android.content.Context;
25 import android.graphics.drawable.ColorDrawable;
26 import android.graphics.drawable.Drawable;
27 import android.net.Uri;
28 import android.util.Size;
29 
30 import com.android.car.apps.common.R;
31 import com.android.car.apps.common.UriUtils;
32 
33 import java.util.Objects;
34 import java.util.function.BiConsumer;
35 import java.util.function.Consumer;
36 
37 /**
38  * A helper class to bind an image to a UI element, updating the image when needed.
39  * @param <T> see {@link ImageRef}.
40  */
41 public class ImageBinder<T extends ImageBinder.ImageRef> {
42 
43     public enum PlaceholderType {
44         /** For elements that don't want to display a placeholder (like tabs). */
45         NONE,
46         /** A placeholder displayed in the foreground, typically has more details. */
47         FOREGROUND,
48         /** A placeholder displayed in the background, typically has less details. */
49         BACKGROUND
50     }
51 
52     /**
53      * Interface to define keys for identifying images.
54      */
55     public interface ImageRef {
56 
57         /** Returns whether the given {@link ImageRef} and this one reference the same image. */
equals(Context context, Object other)58         boolean equals(Context context, Object other);
59 
60         /** Returns the uri to use to retrieve the image. */
getImageURI()61         @Nullable Uri getImageURI();
62 
63         /** For when the image ref doesn't always use a uri. */
getImage(Context context)64         default @Nullable Drawable getImage(Context context) {
65             return null;
66         }
67 
68         /** Returns a placeholder for images that can't be found. */
getPlaceholder(Context context, @NonNull PlaceholderType type)69         Drawable getPlaceholder(Context context, @NonNull PlaceholderType type);
70     }
71 
72     private final PlaceholderType mPlaceholderType;
73     private final Size mMaxImageSize;
74     @Nullable
75     private final Consumer<Drawable> mClient;
76 
77     private T mCurrentRef;
78     private ImageKey mCurrentKey;
79     private BiConsumer<ImageKey, Drawable> mFetchReceiver;
80     private Drawable mLoadingDrawable;
81 
82 
ImageBinder(@onNull PlaceholderType type, @NonNull Size maxImageSize, @NonNull Consumer<Drawable> consumer)83     public ImageBinder(@NonNull PlaceholderType type, @NonNull Size maxImageSize,
84             @NonNull Consumer<Drawable> consumer) {
85         mPlaceholderType = checkNotNull(type, "Need a type");
86         mMaxImageSize = checkNotNull(maxImageSize, "Need a size");
87         mClient = checkNotNull(consumer, "Cannot bind a null consumer");
88     }
89 
ImageBinder(@onNull PlaceholderType type, @NonNull Size maxImageSize)90     protected ImageBinder(@NonNull PlaceholderType type, @NonNull Size maxImageSize) {
91         mPlaceholderType = checkNotNull(type, "Need a type");
92         mMaxImageSize = checkNotNull(maxImageSize, "Need a size");
93         mClient = null;
94     }
95 
setDrawable(@ullable Drawable drawable)96     protected void setDrawable(@Nullable Drawable drawable) {
97         if (mClient != null) {
98             mClient.accept(drawable);
99         }
100     }
101 
102     /** Fetches a new image if needed. */
setImage(Context context, @Nullable T newRef)103     public void setImage(Context context, @Nullable T newRef) {
104         if (isSameImage(context, newRef)) {
105             return;
106         }
107 
108         prepareForNewBinding(context);
109 
110         mCurrentRef = newRef;
111 
112         if (mCurrentRef == null) {
113             setDrawable(null);
114         } else {
115             Drawable image = mCurrentRef.getImage(context);
116             if (image != null) {
117                 setDrawable(image);
118                 return;
119             }
120 
121             mFetchReceiver = (key, drawable) -> {
122                 if (Objects.equals(mCurrentKey, key)) {
123                     Drawable displayed =
124                             (drawable == null && mPlaceholderType != PlaceholderType.NONE)
125                                     ? mCurrentRef.getPlaceholder(context, mPlaceholderType)
126                                     : drawable;
127                     setDrawable(displayed);
128                     onRequestFinished();
129                 }
130             };
131 
132             if (UriUtils.isEmpty(mCurrentRef.getImageURI())) {
133                 mCurrentKey = null;
134                 mFetchReceiver.accept(null, null);
135             } else {
136                 mCurrentKey = new ImageKey(mCurrentRef.getImageURI(), mMaxImageSize);
137                 getImageFetcher(context).getImage(context, mCurrentKey, mFetchReceiver);
138             }
139         }
140     }
141 
isSameImage(Context context, @Nullable T newRef)142     private boolean isSameImage(Context context, @Nullable T newRef) {
143         if (mCurrentRef == null && newRef == null) return true;
144 
145         if (mCurrentRef != null && newRef != null) {
146             return mCurrentRef.equals(context, newRef);
147         }
148 
149         return false;
150     }
151 
getImageFetcher(Context context)152     private LocalImageFetcher getImageFetcher(Context context) {
153         return LocalImageFetcher.getInstance(context);
154     }
155 
prepareForNewBinding(Context context)156     protected void prepareForNewBinding(Context context) {
157         if (mCurrentKey != null) {
158             getImageFetcher(context).cancelRequest(mCurrentKey, mFetchReceiver);
159             onRequestFinished();
160         }
161         setDrawable(mPlaceholderType != PlaceholderType.NONE ? getLoadingDrawable(context) : null);
162     }
163 
onRequestFinished()164     private void onRequestFinished() {
165         mCurrentKey = null;
166         mFetchReceiver = null;
167     }
168 
getLoadingDrawable(Context context)169     private Drawable getLoadingDrawable(Context context) {
170         if (mLoadingDrawable == null) {
171             int color = context.getColor(R.color.loading_image_placeholder_color);
172             mLoadingDrawable = new ColorDrawable(color);
173         }
174         return mLoadingDrawable;
175     }
176 }
177