blob: 1946006befcd1f4f9550b5dab29c2fd185e30da6 [file] [log] [blame]
/*
* Copyright (C) 2019 The Android Open Source 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.car.dialer.notification;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.Observer;
import com.android.car.arch.common.LiveDataFunctions;
import com.android.car.dialer.Constants;
import com.android.car.dialer.R;
import com.android.car.dialer.bluetooth.UiBluetoothMonitor;
import com.android.car.dialer.livedata.UnreadMissedCallLiveData;
import com.android.car.dialer.log.L;
import com.android.car.dialer.ui.TelecomActivity;
import com.android.car.dialer.ui.TelecomPageTab;
import com.android.car.telephony.common.PhoneCallLog;
import com.android.car.telephony.common.TelecomUtils;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
/** Controller that manages the missed call notifications. */
public final class MissedCallNotificationController {
private static final String TAG = "CD.MissedCallNotification";
private static final String CHANNEL_ID = "com.android.car.dialer.missedcall";
// A random number that is used for notification id.
private static final int NOTIFICATION_ID = 20190520;
private static MissedCallNotificationController sMissedCallNotificationController;
/**
* Initialized a globally accessible {@link MissedCallNotificationController} which can be
* retrieved by {@link #get}. If this function is called a second time before calling {@link
* #tearDown()}, an {@link IllegalStateException} will be thrown.
*
* @param applicationContext Application context.
*/
public static void init(Context applicationContext) {
if (sMissedCallNotificationController == null) {
sMissedCallNotificationController = new MissedCallNotificationController(
applicationContext);
} else {
throw new IllegalStateException(
"MissedCallNotificationController has been initialized.");
}
}
/**
* Gets the global {@link MissedCallNotificationController} instance. Make sure {@link
* #init(Context)} is called before calling this method.
*/
public static MissedCallNotificationController get() {
if (sMissedCallNotificationController == null) {
throw new IllegalStateException(
"Call MissedCallNotificationController.init(Context) before calling this "
+ "function");
}
return sMissedCallNotificationController;
}
/** Tear down the global missed call notification controller. */
public void tearDown() {
mUnreadMissedCallLiveData.removeObserver(mUnreadMissedCallObserver);
sMissedCallNotificationController = null;
}
private final Context mContext;
private final NotificationManager mNotificationManager;
private final LiveData<List<PhoneCallLog>> mUnreadMissedCallLiveData;
private final Observer<List<PhoneCallLog>> mUnreadMissedCallObserver;
private final List<PhoneCallLog> mCurrentPhoneCallLogList;
private final Map<String, CompletableFuture<Void>> mUpdateFutures = new HashMap<>();
private MissedCallNotificationController(Context context) {
mContext = context;
mNotificationManager =
(NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
CharSequence name = mContext.getString(R.string.missed_call_notification_channel_name);
NotificationChannel notificationChannel = new NotificationChannel(CHANNEL_ID, name,
NotificationManager.IMPORTANCE_DEFAULT);
mNotificationManager.createNotificationChannel(notificationChannel);
mCurrentPhoneCallLogList = new ArrayList<>();
mUnreadMissedCallLiveData = LiveDataFunctions.switchMapNonNull(
UiBluetoothMonitor.get().getFirstHfpConnectedDevice(),
device-> UnreadMissedCallLiveData.newInstance(context, device.getAddress()));
mUnreadMissedCallObserver = this::updateNotifications;
mUnreadMissedCallLiveData.observeForever(mUnreadMissedCallObserver);
}
/**
* The phone call log list might be null when switching users if permission gets denied and
* throws exception.
*/
private void updateNotifications(@Nullable List<PhoneCallLog> phoneCallLogs) {
List<PhoneCallLog> updatedPhoneCallLogs =
phoneCallLogs == null ? Collections.emptyList() : phoneCallLogs;
for (PhoneCallLog phoneCallLog : updatedPhoneCallLogs) {
showMissedCallNotification(phoneCallLog);
mCurrentPhoneCallLogList.remove(phoneCallLog);
}
for (PhoneCallLog phoneCallLog : mCurrentPhoneCallLogList) {
cancelMissedCallNotification(phoneCallLog);
}
mCurrentPhoneCallLogList.clear();
mCurrentPhoneCallLogList.addAll(updatedPhoneCallLogs);
}
private void showMissedCallNotification(PhoneCallLog callLog) {
L.d(TAG, "show missed call notification %s", callLog);
String phoneNumber = callLog.getPhoneNumberString();
String tag = getTag(callLog);
cancelLoadingRunnable(tag);
CompletableFuture<Void> updateFuture = NotificationUtils.getDisplayNameAndRoundedAvatar(
mContext, phoneNumber)
.thenAcceptAsync((pair) -> {
int callLogSize = callLog.getAllCallRecords().size();
Notification.Builder builder = new Notification.Builder(mContext, CHANNEL_ID)
.setSmallIcon(R.drawable.ic_phone)
.setLargeIcon(pair.second)
.setContentTitle(mContext.getResources().getQuantityString(
R.plurals.notification_missed_call, callLogSize, callLogSize))
.setContentText(TelecomUtils.getBidiWrappedNumber(pair.first))
.setContentIntent(getContentPendingIntent())
.setDeleteIntent(getDeleteIntent())
.setOnlyAlertOnce(true)
.setShowWhen(true)
.setWhen(callLog.getLastCallEndTimestamp())
.setAutoCancel(false);
if (!TextUtils.isEmpty(phoneNumber)) {
builder.addAction(getAction(phoneNumber, R.string.call_back,
NotificationService.ACTION_CALL_BACK_MISSED));
// TODO: add action button to send message
}
mNotificationManager.notify(
tag,
NOTIFICATION_ID,
builder.build());
}, mContext.getMainExecutor());
mUpdateFutures.put(tag, updateFuture);
}
private void cancelMissedCallNotification(PhoneCallLog phoneCallLog) {
L.d(TAG, "cancel missed call notification %s", phoneCallLog);
String tag = getTag(phoneCallLog);
cancelLoadingRunnable(tag);
mNotificationManager.cancel(tag, NOTIFICATION_ID);
}
private void cancelLoadingRunnable(String tag) {
CompletableFuture<Void> completableFuture = mUpdateFutures.get(tag);
if (completableFuture != null) {
completableFuture.cancel(true);
}
mUpdateFutures.remove(tag);
}
private PendingIntent getContentPendingIntent() {
Intent intent = new Intent(mContext, TelecomActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.setAction(Constants.Intents.ACTION_SHOW_PAGE);
intent.putExtra(Constants.Intents.EXTRA_SHOW_PAGE, TelecomPageTab.Page.CALL_HISTORY);
intent.putExtra(Constants.Intents.EXTRA_ACTION_READ_MISSED, true);
PendingIntent pendingIntent = PendingIntent.getActivity(mContext, 0, intent,
PendingIntent.FLAG_UPDATE_CURRENT);
return pendingIntent;
}
private PendingIntent getDeleteIntent() {
Intent intent = new Intent(NotificationService.ACTION_READ_ALL_MISSED, null, mContext,
NotificationService.class);
PendingIntent pendingIntent = PendingIntent.getService(
mContext,
0,
intent,
PendingIntent.FLAG_UPDATE_CURRENT);
return pendingIntent;
}
private Notification.Action getAction(String phoneNumberString, @StringRes int actionText,
String intentAction) {
CharSequence text = mContext.getString(actionText);
PendingIntent intent = getIntent(intentAction, phoneNumberString);
return new Notification.Action.Builder(null, text, intent).build();
}
private PendingIntent getIntent(String action, String phoneNumberString) {
Intent intent = new Intent(action, null, mContext, NotificationService.class);
intent.putExtra(NotificationService.EXTRA_CALL_ID, phoneNumberString);
return PendingIntent.getService(
mContext,
// Unique id for PendingIntents with different extras
/* requestCode= */(int) System.currentTimeMillis(),
intent,
PendingIntent.FLAG_IMMUTABLE);
}
private String getTag(@NonNull PhoneCallLog phoneCallLog) {
return String.valueOf(phoneCallLog.hashCode());
}
}