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