blob: 84a5563ed2ead2c61f8806b11877fd54ea29fa62 [file] [log] [blame]
/*
* Copyright (C) 2008 The Android Open Source Project
* Copyright (C) 2023 The LineageOS Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.incallui;
import android.app.Notification;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import androidx.annotation.MainThread;
import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
import com.android.dialer.R;
import com.android.dialer.common.LogUtil;
import com.android.dialer.common.concurrent.DialerExecutor;
import com.android.dialer.common.concurrent.DialerExecutorComponent;
import java.io.IOException;
import java.io.InputStream;
/** Helper class for loading contacts photo asynchronously. */
public class ContactsAsyncHelper {
/** Interface for a WorkerHandler result return. */
interface OnImageLoadCompleteListener {
/**
* Called when the image load is complete. Must be called in main thread.
*
* @param token Integer passed in {@link ContactsAsyncHelper#startObtainPhotoAsync(int, Context,
* Uri, OnImageLoadCompleteListener, Object)}.
* @param photo Drawable object obtained by the async load.
* @param photoIcon Bitmap object obtained by the async load.
* @param cookie Object passed in {@link ContactsAsyncHelper#startObtainPhotoAsync(int, Context,
* Uri, OnImageLoadCompleteListener, Object)}. Can be null iff. the original cookie is null.
*/
@MainThread
void onImageLoadComplete(int token, Drawable photo, Bitmap photoIcon, Object cookie);
/** Called when image is loaded to udpate data. Must be called in worker thread. */
@WorkerThread
void onImageLoaded(int token, Drawable photo, Bitmap photoIcon, Object cookie);
}
/**
* Starts an asynchronous image load. After finishing the load, {@link
* OnImageLoadCompleteListener#onImageLoadComplete(int, Drawable, Bitmap, Object)} will be called.
*
* @param token Arbitrary integer which will be returned as the first argument of {@link
* OnImageLoadCompleteListener#onImageLoadComplete(int, Drawable, Bitmap, Object)}
* @param context Context object used to do the time-consuming operation.
* @param displayPhotoUri Uri to be used to fetch the photo
* @param listener Callback object which will be used when the asynchronous load is done. Can be
* null, which means only the asynchronous load is done while there's no way to obtain the
* loaded photos.
* @param cookie Arbitrary object the caller wants to remember, which will become the fourth
* argument of {@link OnImageLoadCompleteListener#onImageLoadComplete(int, Drawable, Bitmap,
* Object)}. Can be null, at which the callback will also has null for the argument.
*/
static void startObtainPhotoAsync(
int token,
Context context,
Uri displayPhotoUri,
OnImageLoadCompleteListener listener,
Object cookie) {
// in case the source caller info is null, the URI will be null as well.
// just update using the placeholder image in this case.
if (displayPhotoUri == null) {
LogUtil.e("ContactsAsyncHelper.startObjectPhotoAsync", "uri is missing");
return;
}
// Added additional Cookie field in the callee to handle arguments
// sent to the callback function.
// setup arguments
WorkerArgs args = new WorkerArgs();
args.token = token;
args.cookie = cookie;
args.context = context;
args.displayPhotoUri = displayPhotoUri;
args.listener = listener;
DialerExecutorComponent.get(context)
.dialerExecutorFactory()
.createNonUiTaskBuilder(new Worker())
.onSuccess(
output -> {
if (args.listener != null) {
LogUtil.d(
"ContactsAsyncHelper.startObtainPhotoAsync",
"notifying listener: "
+ args.listener
+ " image: "
+ args.displayPhotoUri
+ " completed");
args.listener.onImageLoadComplete(
args.token, args.photo, args.photoIcon, args.cookie);
}
})
.build()
.executeParallel(args);
}
private static final class WorkerArgs {
public int token;
public Context context;
public Uri displayPhotoUri;
public Drawable photo;
public Bitmap photoIcon;
public Object cookie;
public OnImageLoadCompleteListener listener;
}
private static class Worker implements DialerExecutor.Worker<WorkerArgs, Void> {
@Nullable
@Override
public Void doInBackground(WorkerArgs args) throws Throwable {
InputStream inputStream = null;
try {
try {
inputStream = args.context.getContentResolver().openInputStream(args.displayPhotoUri);
} catch (Exception e) {
LogUtil.e(
"ContactsAsyncHelper.Worker.doInBackground", "error opening photo input stream", e);
}
if (inputStream != null) {
args.photo = Drawable.createFromStream(inputStream, args.displayPhotoUri.toString());
// This assumes Drawable coming from contact database is usually
// BitmapDrawable and thus we can have (down)scaled version of it.
args.photoIcon = getPhotoIconWhenAppropriate(args.context, args.photo);
LogUtil.d(
"ContactsAsyncHelper.Worker.doInBackground",
"loading image, URI: %s",
args.displayPhotoUri);
} else {
args.photo = null;
args.photoIcon = null;
LogUtil.d(
"ContactsAsyncHelper.Worker.doInBackground",
"problem with image, URI: %s, using default image.",
args.displayPhotoUri);
}
if (args.listener != null) {
args.listener.onImageLoaded(args.token, args.photo, args.photoIcon, args.cookie);
}
} finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
LogUtil.e(
"ContactsAsyncHelper.Worker.doInBackground", "Unable to close input stream.", e);
}
}
}
return null;
}
/**
* Returns a Bitmap object suitable for {@link Notification}'s large icon. This might return
* null when the given Drawable isn't BitmapDrawable, or if the system fails to create a scaled
* Bitmap for the Drawable.
*/
private Bitmap getPhotoIconWhenAppropriate(Context context, Drawable photo) {
if (!(photo instanceof BitmapDrawable)) {
return null;
}
int iconSize = context.getResources().getDimensionPixelSize(R.dimen.notification_icon_size);
Bitmap orgBitmap = ((BitmapDrawable) photo).getBitmap();
int orgWidth = orgBitmap.getWidth();
int orgHeight = orgBitmap.getHeight();
int longerEdge = orgWidth > orgHeight ? orgWidth : orgHeight;
// We want downscaled one only when the original icon is too big.
if (longerEdge > iconSize) {
float ratio = ((float) longerEdge) / iconSize;
int newWidth = (int) (orgWidth / ratio);
int newHeight = (int) (orgHeight / ratio);
// If the longer edge is much longer than the shorter edge, the latter may
// become 0 which will cause a crash.
if (newWidth <= 0 || newHeight <= 0) {
LogUtil.w(
"ContactsAsyncHelper.Worker.getPhotoIconWhenAppropriate",
"Photo icon's width or height become 0.");
return null;
}
// It is sure ratio >= 1.0f in any case and thus the newly created Bitmap
// should be smaller than the original.
return Bitmap.createScaledBitmap(orgBitmap, newWidth, newHeight, true);
} else {
return orgBitmap;
}
}
}
}