• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2021 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.server.nearby.fastpair.cache;
18 
19 import static com.android.server.nearby.common.bluetooth.fastpair.BluetoothAddress.maskBluetoothAddress;
20 import static com.android.server.nearby.fastpair.UserActionHandler.EXTRA_FAST_PAIR_SECRET;
21 
22 import android.annotation.IntDef;
23 import android.annotation.Nullable;
24 import android.content.Context;
25 import android.content.Intent;
26 import android.graphics.Bitmap;
27 import android.graphics.BitmapFactory;
28 import android.text.TextUtils;
29 import android.util.Log;
30 
31 import com.android.internal.annotations.VisibleForTesting;
32 import com.android.server.nearby.common.ble.util.RangingUtils;
33 import com.android.server.nearby.common.fastpair.IconUtils;
34 import com.android.server.nearby.common.locator.Locator;
35 import com.android.server.nearby.common.locator.LocatorContextWrapper;
36 
37 import java.lang.annotation.Retention;
38 import java.lang.annotation.RetentionPolicy;
39 import java.net.URISyntaxException;
40 import java.time.Clock;
41 import java.util.Objects;
42 
43 import service.proto.Cache;
44 
45 /**
46  * Wrapper class around StoredDiscoveryItem. A centralized place for methods related to
47  * updating/parsing StoredDiscoveryItem.
48  */
49 public class DiscoveryItem implements Comparable<DiscoveryItem> {
50 
51     private static final String ACTION_FAST_PAIR =
52             "com.android.server.nearby:ACTION_FAST_PAIR";
53     private static final int BEACON_STALENESS_MILLIS = 120000;
54     private static final int ITEM_EXPIRATION_MILLIS = 20000;
55     private static final int APP_INSTALL_EXPIRATION_MILLIS = 600000;
56     private static final int ITEM_DELETABLE_MILLIS = 15000;
57 
58     private final FastPairCacheManager mFastPairCacheManager;
59     private final Clock mClock;
60 
61     private Cache.StoredDiscoveryItem mStoredDiscoveryItem;
62 
63     /** IntDef for StoredDiscoveryItem.State */
64     @IntDef({
65             Cache.StoredDiscoveryItem.State.STATE_ENABLED_VALUE,
66             Cache.StoredDiscoveryItem.State.STATE_MUTED_VALUE,
67             Cache.StoredDiscoveryItem.State.STATE_DISABLED_BY_SYSTEM_VALUE
68     })
69     @Retention(RetentionPolicy.SOURCE)
70     public @interface ItemState {
71     }
72 
DiscoveryItem(LocatorContextWrapper locatorContextWrapper, Cache.StoredDiscoveryItem mStoredDiscoveryItem)73     public DiscoveryItem(LocatorContextWrapper locatorContextWrapper,
74             Cache.StoredDiscoveryItem mStoredDiscoveryItem) {
75         this.mFastPairCacheManager =
76                 locatorContextWrapper.getLocator().get(FastPairCacheManager.class);
77         this.mClock =
78                 locatorContextWrapper.getLocator().get(Clock.class);
79         this.mStoredDiscoveryItem = mStoredDiscoveryItem;
80     }
81 
DiscoveryItem(Context context, Cache.StoredDiscoveryItem mStoredDiscoveryItem)82     public DiscoveryItem(Context context, Cache.StoredDiscoveryItem mStoredDiscoveryItem) {
83         this.mFastPairCacheManager = Locator.get(context, FastPairCacheManager.class);
84         this.mClock = Locator.get(context, Clock.class);
85         this.mStoredDiscoveryItem = mStoredDiscoveryItem;
86     }
87 
88     /** @return A new StoredDiscoveryItem with state fields set to their defaults. */
newStoredDiscoveryItem()89     public static Cache.StoredDiscoveryItem newStoredDiscoveryItem() {
90         Cache.StoredDiscoveryItem.Builder storedDiscoveryItem =
91                 Cache.StoredDiscoveryItem.newBuilder();
92         storedDiscoveryItem.setState(Cache.StoredDiscoveryItem.State.STATE_ENABLED);
93         return storedDiscoveryItem.build();
94     }
95 
96     /**
97      * Checks if store discovery item support fast pair or not.
98      */
isFastPair()99     public boolean isFastPair() {
100         Intent intent = parseIntentScheme(mStoredDiscoveryItem.getActionUrl());
101         if (intent == null) {
102             Log.w("FastPairDiscovery", "FastPair: fail to parse action url"
103                     + mStoredDiscoveryItem.getActionUrl());
104             return false;
105         }
106         return ACTION_FAST_PAIR.equals(intent.getAction());
107     }
108 
109     /**
110      * Checks if the item is expired. Expired items are those over getItemExpirationMillis() eg. 2
111      * minutes
112      */
isExpired( long currentTimestampMillis, @Nullable Long lastObservationTimestampMillis)113     public static boolean isExpired(
114             long currentTimestampMillis, @Nullable Long lastObservationTimestampMillis) {
115         if (lastObservationTimestampMillis == null) {
116             return true;
117         }
118         return (currentTimestampMillis - lastObservationTimestampMillis)
119                 >= ITEM_EXPIRATION_MILLIS;
120     }
121 
122     /**
123      * Checks if the item is deletable for saving disk space. Deletable items are those over
124      * getItemDeletableMillis eg. over 25 hrs.
125      */
isDeletable( long currentTimestampMillis, @Nullable Long lastObservationTimestampMillis)126     public static boolean isDeletable(
127             long currentTimestampMillis, @Nullable Long lastObservationTimestampMillis) {
128         if (lastObservationTimestampMillis == null) {
129             return true;
130         }
131         return currentTimestampMillis - lastObservationTimestampMillis
132                 >= ITEM_DELETABLE_MILLIS;
133     }
134 
135     /** Checks if the item has a pending app install */
isPendingAppInstallValid()136     public boolean isPendingAppInstallValid() {
137         return isPendingAppInstallValid(mClock.millis());
138     }
139 
140     /**
141      * Checks if pending app valid.
142      */
isPendingAppInstallValid(long appInstallMillis)143     public boolean isPendingAppInstallValid(long appInstallMillis) {
144         return isPendingAppInstallValid(appInstallMillis, mStoredDiscoveryItem);
145     }
146 
147     /**
148      * Checks if the app install time expired.
149      */
isPendingAppInstallValid( long currentMillis, Cache.StoredDiscoveryItem storedItem)150     public static boolean isPendingAppInstallValid(
151             long currentMillis, Cache.StoredDiscoveryItem storedItem) {
152         return currentMillis - storedItem.getPendingAppInstallTimestampMillis()
153                 < APP_INSTALL_EXPIRATION_MILLIS;
154     }
155 
156 
157     /** Checks if the item has enough data to be shown */
isReadyForDisplay()158     public boolean isReadyForDisplay() {
159         boolean hasUrlOrPopularApp = !mStoredDiscoveryItem.getActionUrl().isEmpty();
160 
161         return !TextUtils.isEmpty(mStoredDiscoveryItem.getTitle()) && hasUrlOrPopularApp;
162     }
163 
164     /** Checks if the action url is app install */
isApp()165     public boolean isApp() {
166         return mStoredDiscoveryItem.getActionUrlType() == Cache.ResolvedUrlType.APP;
167     }
168 
169     /** Returns true if an item is muted, or if state is unavailable. */
isMuted()170     public boolean isMuted() {
171         return mStoredDiscoveryItem.getState() != Cache.StoredDiscoveryItem.State.STATE_ENABLED;
172     }
173 
174     /**
175      * Returns the state of store discovery item.
176      */
getState()177     public Cache.StoredDiscoveryItem.State getState() {
178         return mStoredDiscoveryItem.getState();
179     }
180 
181     /** Checks if it's device item. e.g. Chromecast / Wear */
isDeviceType(Cache.NearbyType type)182     public static boolean isDeviceType(Cache.NearbyType type) {
183         return type == Cache.NearbyType.NEARBY_CHROMECAST
184                 || type == Cache.NearbyType.NEARBY_WEAR
185                 || type == Cache.NearbyType.NEARBY_DEVICE;
186     }
187 
188     /**
189      * Check if the type is supported.
190      */
isTypeEnabled(Cache.NearbyType type)191     public static boolean isTypeEnabled(Cache.NearbyType type) {
192         switch (type) {
193             case NEARBY_WEAR:
194             case NEARBY_CHROMECAST:
195             case NEARBY_DEVICE:
196                 return true;
197             default:
198                 Log.e("FastPairDiscoveryItem", "Invalid item type " + type.name());
199                 return false;
200         }
201     }
202 
203     /** Gets hash code of UI related data so we can collapse identical items. */
getUiHashCode()204     public int getUiHashCode() {
205         return Objects.hash(
206                         mStoredDiscoveryItem.getTitle(),
207                         mStoredDiscoveryItem.getDescription(),
208                         mStoredDiscoveryItem.getAppName(),
209                         mStoredDiscoveryItem.getDisplayUrl(),
210                         mStoredDiscoveryItem.getMacAddress());
211     }
212 
213     // Getters below
214 
215     /**
216      * Returns the id of store discovery item.
217      */
218     @Nullable
getId()219     public String getId() {
220         return mStoredDiscoveryItem.getId();
221     }
222 
223     /**
224      * Returns the title of discovery item.
225      */
226     @Nullable
getTitle()227     public String getTitle() {
228         return mStoredDiscoveryItem.getTitle();
229     }
230 
231     /**
232      * Returns the description of discovery item.
233      */
234     @Nullable
getDescription()235     public String getDescription() {
236         return mStoredDiscoveryItem.getDescription();
237     }
238 
239     /**
240      * Returns the mac address of discovery item.
241      */
242     @Nullable
getMacAddress()243     public String getMacAddress() {
244         return mStoredDiscoveryItem.getMacAddress();
245     }
246 
247     /**
248      * Returns the display url of discovery item.
249      */
250     @Nullable
getDisplayUrl()251     public String getDisplayUrl() {
252         return mStoredDiscoveryItem.getDisplayUrl();
253     }
254 
255     /**
256      * Returns the public key of discovery item.
257      */
258     @Nullable
getAuthenticationPublicKeySecp256R1()259     public byte[] getAuthenticationPublicKeySecp256R1() {
260         return mStoredDiscoveryItem.getAuthenticationPublicKeySecp256R1().toByteArray();
261     }
262 
263     /**
264      * Returns the pairing secret.
265      */
266     @Nullable
getFastPairSecretKey()267     public String getFastPairSecretKey() {
268         Intent intent = parseIntentScheme(mStoredDiscoveryItem.getActionUrl());
269         if (intent == null) {
270             Log.d("FastPairDiscoveryItem", "FastPair: fail to parse action url "
271                     + mStoredDiscoveryItem.getActionUrl());
272             return null;
273         }
274         return intent.getStringExtra(EXTRA_FAST_PAIR_SECRET);
275     }
276 
277     /**
278      * Returns the fast pair info of discovery item.
279      */
280     @Nullable
getFastPairInformation()281     public Cache.FastPairInformation getFastPairInformation() {
282         return mStoredDiscoveryItem.hasFastPairInformation()
283                 ? mStoredDiscoveryItem.getFastPairInformation() : null;
284     }
285 
286     /**
287      * Returns the app name of discovery item.
288      */
289     @Nullable
290     @VisibleForTesting
getAppName()291     protected String getAppName() {
292         return mStoredDiscoveryItem.getAppName();
293     }
294 
295     /**
296      * Returns the package name of discovery item.
297      */
298     @Nullable
getAppPackageName()299     public String getAppPackageName() {
300         return mStoredDiscoveryItem.getPackageName();
301     }
302 
303     /**
304      * Returns the action url of discovery item.
305      */
306     @Nullable
getActionUrl()307     public String getActionUrl() {
308         return mStoredDiscoveryItem.getActionUrl();
309     }
310 
311     /**
312      * Returns the rssi value of discovery item.
313      */
314     @Nullable
getRssi()315     public Integer getRssi() {
316         return mStoredDiscoveryItem.getRssi();
317     }
318 
319     /**
320      * Returns the TX power of discovery item.
321      */
322     @Nullable
getTxPower()323     public Integer getTxPower() {
324         return mStoredDiscoveryItem.getTxPower();
325     }
326 
327     /**
328      * Returns the first observed time stamp of discovery item.
329      */
330     @Nullable
getFirstObservationTimestampMillis()331     public Long getFirstObservationTimestampMillis() {
332         return mStoredDiscoveryItem.getFirstObservationTimestampMillis();
333     }
334 
335     /**
336      * Returns the last observed time stamp of discovery item.
337      */
338     @Nullable
getLastObservationTimestampMillis()339     public Long getLastObservationTimestampMillis() {
340         return mStoredDiscoveryItem.getLastObservationTimestampMillis();
341     }
342 
343     /**
344      * Calculates an estimated distance for the item, computed from the TX power (at 1m) and RSSI.
345      *
346      * @return estimated distance, or null if there is no RSSI or no TX power.
347      */
348     @Nullable
getEstimatedDistance()349     public Double getEstimatedDistance() {
350         // In the future, we may want to do a foreground subscription to leverage onDistanceChanged.
351         return RangingUtils.distanceFromRssiAndTxPower(mStoredDiscoveryItem.getRssi(),
352                 mStoredDiscoveryItem.getTxPower());
353     }
354 
355     /**
356      * Gets icon Bitmap from icon store.
357      *
358      * @return null if no icon or icon size is incorrect.
359      */
360     @Nullable
getIcon()361     public Bitmap getIcon() {
362         Bitmap icon =
363                 BitmapFactory.decodeByteArray(
364                         mStoredDiscoveryItem.getIconPng().toByteArray(),
365                         0 /* offset */, mStoredDiscoveryItem.getIconPng().size());
366         if (IconUtils.isIconSizeCorrect(icon)) {
367             return icon;
368         } else {
369             return null;
370         }
371     }
372 
373     /** Gets a FIFE URL of the icon. */
374     @Nullable
getIconFifeUrl()375     public String getIconFifeUrl() {
376         return mStoredDiscoveryItem.getIconFifeUrl();
377     }
378 
379     /**
380      * Compares this object to the specified object: 1. By device type. Device setups are 'greater
381      * than' beacons. 2. By relevance. More relevant items are 'greater than' less relevant items.
382      * 3.By distance. Nearer items are 'greater than' further items.
383      *
384      * <p>In the list view, we sort in descending order, i.e. we put the most relevant items first.
385      */
386     @Override
compareTo(DiscoveryItem another)387     public int compareTo(DiscoveryItem another) {
388         // For items of the same relevance, compare distance.
389         Double distance1 = getEstimatedDistance();
390         Double distance2 = another.getEstimatedDistance();
391         distance1 = distance1 != null ? distance1 : Double.MAX_VALUE;
392         distance2 = distance2 != null ? distance2 : Double.MAX_VALUE;
393         // Negate because closer items are better ("greater than") further items.
394         return -distance1.compareTo(distance2);
395     }
396 
397     @Nullable
getTriggerId()398     public String getTriggerId() {
399         return mStoredDiscoveryItem.getTriggerId();
400     }
401 
402     @Override
equals(Object another)403     public boolean equals(Object another) {
404         if (another instanceof DiscoveryItem) {
405             return ((DiscoveryItem) another).mStoredDiscoveryItem.equals(mStoredDiscoveryItem);
406         }
407         return false;
408     }
409 
410     @Override
hashCode()411     public int hashCode() {
412         return mStoredDiscoveryItem.hashCode();
413     }
414 
415     @Override
toString()416     public String toString() {
417         return String.format(
418                 "[triggerId=%s], [id=%s], [title=%s], [url=%s], [ready=%s], [macAddress=%s]",
419                 getTriggerId(),
420                 getId(),
421                 getTitle(),
422                 getActionUrl(),
423                 isReadyForDisplay(),
424                 maskBluetoothAddress(getMacAddress()));
425     }
426 
427     /**
428      * Gets a copy of the StoredDiscoveryItem proto backing this DiscoveryItem. Currently needed for
429      * Fast Pair 2.0: We store the item in the cloud associated with a user's account, to enable
430      * pairing with other devices owned by the user.
431      */
getCopyOfStoredItem()432     public Cache.StoredDiscoveryItem getCopyOfStoredItem() {
433         return mStoredDiscoveryItem;
434     }
435 
436     /**
437      * Gets the StoredDiscoveryItem represented by this DiscoveryItem. This lets tests manipulate
438      * values that production code should not manipulate.
439      */
440 
getStoredItemForTest()441     public Cache.StoredDiscoveryItem getStoredItemForTest() {
442         return mStoredDiscoveryItem;
443     }
444 
445     /**
446      * Sets the StoredDiscoveryItem represented by this DiscoveryItem. This lets tests manipulate
447      * values that production code should not manipulate.
448      */
setStoredItemForTest(Cache.StoredDiscoveryItem s)449     public void setStoredItemForTest(Cache.StoredDiscoveryItem s) {
450         mStoredDiscoveryItem = s;
451     }
452 
453     /**
454      * Parse the intent from item url.
455      */
parseIntentScheme(String uri)456     public static Intent parseIntentScheme(String uri) {
457         try {
458             return Intent.parseUri(uri, Intent.URI_INTENT_SCHEME);
459         } catch (URISyntaxException e) {
460             return null;
461         }
462     }
463 }
464