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