1 /* 2 * Copyright (C) 2018 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.dialer.phonelookup.spam; 18 19 import android.content.Context; 20 import android.content.SharedPreferences; 21 import android.support.annotation.Nullable; 22 import android.support.annotation.VisibleForTesting; 23 import com.android.dialer.DialerPhoneNumber; 24 import com.android.dialer.common.Assert; 25 import com.android.dialer.common.concurrent.Annotations.BackgroundExecutor; 26 import com.android.dialer.common.concurrent.Annotations.LightweightExecutor; 27 import com.android.dialer.phonelookup.PhoneLookup; 28 import com.android.dialer.phonelookup.PhoneLookupInfo; 29 import com.android.dialer.phonelookup.PhoneLookupInfo.SpamInfo; 30 import com.android.dialer.spam.Spam; 31 import com.android.dialer.spam.SpamStatus; 32 import com.android.dialer.storage.Unencrypted; 33 import com.google.common.base.Optional; 34 import com.google.common.collect.ImmutableMap; 35 import com.google.common.collect.ImmutableSet; 36 import com.google.common.util.concurrent.Futures; 37 import com.google.common.util.concurrent.ListenableFuture; 38 import com.google.common.util.concurrent.ListeningExecutorService; 39 import java.util.Map.Entry; 40 import javax.inject.Inject; 41 42 /** PhoneLookup implementation for Spam info. */ 43 public final class SpamPhoneLookup implements PhoneLookup<SpamInfo> { 44 45 @VisibleForTesting 46 static final String PREF_LAST_TIMESTAMP_PROCESSED = "spamPhoneLookupLastTimestampProcessed"; 47 48 private final ListeningExecutorService lightweightExecutorService; 49 private final ListeningExecutorService backgroundExecutorService; 50 private final SharedPreferences sharedPreferences; 51 private final Spam spam; 52 53 @Nullable private Long currentLastTimestampProcessed; 54 55 @Inject SpamPhoneLookup( @ackgroundExecutor ListeningExecutorService backgroundExecutorService, @LightweightExecutor ListeningExecutorService lightweightExecutorService, @Unencrypted SharedPreferences sharedPreferences, Spam spam)56 SpamPhoneLookup( 57 @BackgroundExecutor ListeningExecutorService backgroundExecutorService, 58 @LightweightExecutor ListeningExecutorService lightweightExecutorService, 59 @Unencrypted SharedPreferences sharedPreferences, 60 Spam spam) { 61 this.backgroundExecutorService = backgroundExecutorService; 62 this.lightweightExecutorService = lightweightExecutorService; 63 this.sharedPreferences = sharedPreferences; 64 this.spam = spam; 65 } 66 67 @Override lookup(DialerPhoneNumber dialerPhoneNumber)68 public ListenableFuture<SpamInfo> lookup(DialerPhoneNumber dialerPhoneNumber) { 69 return Futures.transform( 70 spam.batchCheckSpamStatus(ImmutableSet.of(dialerPhoneNumber)), 71 spamStatusMap -> 72 SpamInfo.newBuilder() 73 .setIsSpam(Assert.isNotNull(spamStatusMap.get(dialerPhoneNumber)).isSpam()) 74 .build(), 75 lightweightExecutorService); 76 } 77 78 @Override isDirty(ImmutableSet<DialerPhoneNumber> phoneNumbers)79 public ListenableFuture<Boolean> isDirty(ImmutableSet<DialerPhoneNumber> phoneNumbers) { 80 ListenableFuture<Long> lastTimestampProcessedFuture = 81 backgroundExecutorService.submit( 82 () -> sharedPreferences.getLong(PREF_LAST_TIMESTAMP_PROCESSED, 0L)); 83 84 return Futures.transformAsync( 85 lastTimestampProcessedFuture, spam::dataUpdatedSince, lightweightExecutorService); 86 } 87 88 @Override getMostRecentInfo( ImmutableMap<DialerPhoneNumber, SpamInfo> existingInfoMap)89 public ListenableFuture<ImmutableMap<DialerPhoneNumber, SpamInfo>> getMostRecentInfo( 90 ImmutableMap<DialerPhoneNumber, SpamInfo> existingInfoMap) { 91 currentLastTimestampProcessed = null; 92 93 ListenableFuture<ImmutableMap<DialerPhoneNumber, SpamStatus>> spamStatusMapFuture = 94 spam.batchCheckSpamStatus(existingInfoMap.keySet()); 95 96 return Futures.transform( 97 spamStatusMapFuture, 98 spamStatusMap -> { 99 ImmutableMap.Builder<DialerPhoneNumber, SpamInfo> mostRecentSpamInfo = 100 new ImmutableMap.Builder<>(); 101 102 for (Entry<DialerPhoneNumber, SpamStatus> dialerPhoneNumberAndSpamStatus : 103 spamStatusMap.entrySet()) { 104 DialerPhoneNumber dialerPhoneNumber = dialerPhoneNumberAndSpamStatus.getKey(); 105 SpamStatus spamStatus = dialerPhoneNumberAndSpamStatus.getValue(); 106 mostRecentSpamInfo.put( 107 dialerPhoneNumber, SpamInfo.newBuilder().setIsSpam(spamStatus.isSpam()).build()); 108 109 Optional<Long> timestampMillis = spamStatus.getTimestampMillis(); 110 if (timestampMillis.isPresent()) { 111 currentLastTimestampProcessed = 112 currentLastTimestampProcessed == null 113 ? timestampMillis.get() 114 : Math.max(timestampMillis.get(), currentLastTimestampProcessed); 115 } 116 } 117 118 // If currentLastTimestampProcessed is null, it means none of the numbers in 119 // existingInfoMap has spam status in the underlying data source. 120 // We should set currentLastTimestampProcessed to the current timestamp to avoid 121 // triggering the bulk update flow repeatedly. 122 if (currentLastTimestampProcessed == null) { 123 currentLastTimestampProcessed = System.currentTimeMillis(); 124 } 125 126 return mostRecentSpamInfo.build(); 127 }, 128 lightweightExecutorService); 129 } 130 131 @Override getSubMessage(PhoneLookupInfo phoneLookupInfo)132 public SpamInfo getSubMessage(PhoneLookupInfo phoneLookupInfo) { 133 return phoneLookupInfo.getSpamInfo(); 134 } 135 136 @Override setSubMessage(PhoneLookupInfo.Builder destination, SpamInfo subMessage)137 public void setSubMessage(PhoneLookupInfo.Builder destination, SpamInfo subMessage) { 138 destination.setSpamInfo(subMessage); 139 } 140 141 @Override onSuccessfulBulkUpdate()142 public ListenableFuture<Void> onSuccessfulBulkUpdate() { 143 return backgroundExecutorService.submit( 144 () -> { 145 sharedPreferences 146 .edit() 147 .putLong( 148 PREF_LAST_TIMESTAMP_PROCESSED, Assert.isNotNull(currentLastTimestampProcessed)) 149 .apply(); 150 return null; 151 }); 152 } 153 154 @Override 155 public void registerContentObservers(Context appContext) { 156 // No content observer can be registered as Spam is not based on a content provider. 157 // Each Spam implementation should be responsible for notifying any data changes. 158 } 159 } 160