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