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