/*
* Copyright 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.wear.tiles;
import android.app.Service;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Handler;
import android.os.IBinder;
import android.os.RemoteException;
import android.os.StrictMode;
import android.util.Log;
import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.concurrent.futures.CallbackToFutureAdapter;
import androidx.concurrent.futures.ResolvableFuture;
import androidx.wear.protolayout.ResourceBuilders.Resources;
import androidx.wear.protolayout.expression.proto.VersionProto.VersionInfo;
import androidx.wear.protolayout.proto.DeviceParametersProto.DeviceParameters;
import androidx.wear.protolayout.protobuf.InvalidProtocolBufferException;
import androidx.wear.tiles.EventBuilders.TileAddEvent;
import androidx.wear.tiles.EventBuilders.TileEnterEvent;
import androidx.wear.tiles.EventBuilders.TileLeaveEvent;
import androidx.wear.tiles.EventBuilders.TileRemoveEvent;
import androidx.wear.tiles.RequestBuilders.ResourcesRequest;
import androidx.wear.tiles.RequestBuilders.TileRequest;
import androidx.wear.tiles.TileBuilders.Tile;
import androidx.wear.tiles.TileBuilders.Version;
import androidx.wear.tiles.proto.EventProto;
import androidx.wear.tiles.proto.RequestProto;
import androidx.wear.tiles.proto.TileProto;
import com.google.common.util.concurrent.ListenableFuture;
import java.lang.ref.WeakReference;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import java.util.stream.Collectors;
/**
* Base class for a service providing data for an app tile.
*
* <p>A provider service must implement {@link #onTileRequest} and {@link #onTileResourcesRequest}
* to respond to requests for updates from the system.
*
* <p>The manifest declaration of this service must include an intent filter for {@link
* #ACTION_BIND_TILE_PROVIDER}.
*
* <p>The manifest entry should also include {@code
* android:permission="com.google.android.wearable.permission.BIND_TILE_PROVIDER"} to ensure that
* only the system can bind to it.
*/
public abstract class TileService extends Service {
private static final String TAG = "TileService";
static final VersionInfo DEFAULT_VERSION =
VersionInfo.newBuilder().setMajor(1).setMinor(0).build();
@SuppressWarnings("deprecation") // For backward compatibility
private static final ListenableFuture<androidx.wear.tiles.ResourceBuilders.Resources>
ON_RESOURCES_REQUEST_NOT_IMPLEMENTED =
createFailedFuture(
new UnsupportedOperationException(
"onResourcesRequest not implemented"));
/**
* The intent action used to send update requests to the provider. Tile provider services must
* declare an intent filter for this action in the manifest.
*/
public static final String ACTION_BIND_TILE_PROVIDER =
"androidx.wear.tiles.action.BIND_TILE_PROVIDER";
/** The ID for the Intent extra containing the ID of the Clickable. */
public static final String EXTRA_CLICKABLE_ID = "androidx.wear.tiles.extra.CLICKABLE_ID";
/**
* The name of the metadata key that should contain a drawable to be presented as a Tile
* preview.
*/
public static final String METADATA_PREVIEW_KEY = "androidx.wear.tiles.PREVIEW";
/**
* Name of the SharedPreferences file used for getting the preferences from the application
* context. The preferences are shared by all TileService implementations from the same app and
* store information regarding the tiles considered to be active. The SharedPreferences key is
* the result retrieved from {@link ActiveTileIdentifier#flattenToString} and the value is a
* timestamp.
*
* <p>The presence of a tile in the SharedPreferences means the tile instance is considered to
* be active (in the carousel). An entry will not be added with an invalid timestamp. The
* timestamp value is from when the entry was first recorded and is updated once every {@code
* UPDATE_TILE_TIMESTAMP_PERIOD_MS} on user interactions to indicate it is still active. If the
* timestamp hasn't been updated for longer than {@code INACTIVE_TILE_PERIOD_MS} the tile will
* be considered inactive and will be removed from the preferences so that entries are not left
* in the app's storage indefinitely if an {@link TileService#onTileRemoveEvent} callback,
* signaling the tile has become inactive, is missed.
*/
private static final String ACTIVE_TILES_SHARED_PREF_NAME = "active_tiles_shared_preferences";
/**
* 1 day in milliseconds for the timestamp refresh period indicating the tile instance stored in
* {@code ACTIVE_TILES_SHARED_PREF_NAME} is still active if the tile is acted upon.
*/
private static final long UPDATE_TILE_TIMESTAMP_PERIOD_MS = Duration.ofDays(1).toMillis();
/**
* 60 days in milliseconds for the period after which a tile instances will be removed from
* {@code ACTIVE_TILES_SHARED_PREF_NAME} if timestamp has not been updated since.
*/
private static final long INACTIVE_TILE_PERIOD_MS = Duration.ofDays(60).toMillis();
private static final TimeSourceClockImpl sTimeSourceClock = new TimeSourceClockImpl();
/**
* Called when the system is requesting a new timeline from this Tile Provider. The returned
* future must complete after at most 10 seconds from the moment this method is called (exact
* timeout length subject to change).
*
* <p>Note that this is called from your app's main thread, which is usually also the UI thread.
*
* @param requestParams Parameters about the request. See {@link TileRequest} for more info.
*/
@MainThread
@NonNull
protected abstract ListenableFuture<Tile> onTileRequest(@NonNull TileRequest requestParams);
/**
* Called when the system is requesting a resource bundle from this Tile Provider. The returned
* future must complete after at most 10 seconds from the moment this method is called (exact
* timeout length subject to change).
*
* <p>Note that this is called from your app's main thread, which is usually also the UI thread.
*
* @param requestParams Parameters about the request. See {@link ResourcesRequest} for more
* info.
* @deprecated Use {@link #onTileResourcesRequest} instead.
*/
@MainThread
@NonNull
@Deprecated
protected ListenableFuture<androidx.wear.tiles.ResourceBuilders.Resources> onResourcesRequest(
@NonNull ResourcesRequest requestParams) {
return ON_RESOURCES_REQUEST_NOT_IMPLEMENTED;
}
/**
* Called when the system is requesting a resource bundle from this Tile Provider. This can
* happen on the first time a Tile is being loaded or whenever the resource version requested by
* a Tile (in {@link #onTileRequest}) changes.
*
* <p>The returned future must complete after at most 10 seconds from the moment this method is
* called (exact timeout length subject to change).
*
* <p>Note that this is called from your app's main thread, which is usually also the UI thread.
* If {@link #onTileResourcesRequest} is not implemented, the {@link TileService} will fallback
* to {@link #onResourcesRequest}.
*
* @param requestParams Parameters about the request. See {@link ResourcesRequest} for more
* info.
*/
@MainThread
@NonNull
@SuppressWarnings({"AsyncSuffixFuture", "deprecation"}) // For backward compatibility
protected ListenableFuture<Resources> onTileResourcesRequest(
@NonNull ResourcesRequest requestParams) {
// We are offering a default implementation for onTileResourcesRequest for backward
// compatibility as older clients are overriding onResourcesRequest.
ListenableFuture<androidx.wear.tiles.ResourceBuilders.Resources>
legacyResourcesRequestResult = onResourcesRequest(requestParams);
if (legacyResourcesRequestResult == ON_RESOURCES_REQUEST_NOT_IMPLEMENTED) {
return createFailedFuture(
new UnsupportedOperationException(
"onTileResourcesRequest " + "not implemented."));
}
ResolvableFuture<Resources> result = ResolvableFuture.create();
legacyResourcesRequestResult.addListener(
() -> {
try {
result.set(
Resources.fromProto(legacyResourcesRequestResult.get().toProto()));
} catch (RuntimeException | InterruptedException | ExecutionException e) {
result.setException(e);
}
},
Runnable::run);
return result;
}
/**
* Called when a tile provided by this Tile Provider is added to the carousel.
*
* <p>Note that this is called from your app's main thread, which is usually also the UI thread.
*
* @param requestParams Parameters about the request. See {@link TileAddEvent} for more info.
*/
@MainThread
protected void onTileAddEvent(@NonNull TileAddEvent requestParams) {}
/**
* Called when a tile provided by this Tile Provider is removed from the carousel.
*
* <p>Note that this is called from your app's main thread, which is usually also the UI thread.
*
* @param requestParams Parameters about the request. See {@link TileRemoveEvent} for more info.
*/
@MainThread
protected void onTileRemoveEvent(@NonNull TileRemoveEvent requestParams) {}
/**
* Called when a tile provided by this Tile Provider becomes into view, on screen.
*
* <p>Note that this is called from your app's main thread, which is usually also the UI thread.
*
* @param requestParams Parameters about the request. See {@link TileEnterEvent} for more info.
*/
@MainThread
protected void onTileEnterEvent(@NonNull TileEnterEvent requestParams) {}
/**
* Called when a tile provided by this Tile Provider goes out of view, on screen.
*
* <p>Note that this is called from your app's main thread, which is usually also the UI thread.
*
* @param requestParams Parameters about the request. See {@link TileLeaveEvent} for more info.
*/
@MainThread
protected void onTileLeaveEvent(@NonNull TileLeaveEvent requestParams) {}
/**
* Gets an instance of {@link TileUpdateRequester} to allow a Tile Provider to notify the tile's
* renderer that it should request a new Timeline from this {@link TileService}.
*
* @param context The application context.
*/
@NonNull
public static TileUpdateRequester getUpdater(@NonNull Context context) {
List<TileUpdateRequester> requesters = new ArrayList<>();
requesters.add(new SysUiTileUpdateRequester(context));
requesters.add(new ViewerTileUpdateRequester(context));
return new CompositeTileUpdateRequester(requesters);
}
/**
* Returns the list of active tiles belonging to the passed {@code context}'s package name. A
* tile is considered to be active if it is present in the carousel.
*
* <p>The result reflects the state of carousel at the time the call was made, which might've
* changed by the time the result is received. {@link TileService#onTileAddEvent} and {@link
* TileService#onTileRemoveEvent} should be used instead for live updates.
*
* <p>This method may not always return all tiles present in the carousel. The possibly omitted
* tiles being the pre-installed tiles, all tiles if the user has cleared the app data, or the
* tiles a user hasn't visited in the last 60 days, while tiles removed by an app update may be
* shown as active for 60 days afterwards.
*
* @param context The application context.
* @param executor The executor on which methods should be invoked. To dispatch events through
* the main thread of your application, you can use {@link
* android.content.Context#getMainExecutor()}.
* @return A list of {@link ActiveTileIdentifier} for the tiles belonging to the passed {@code
* context} present in the carousel, or a value based on platform-specific fallback
* behavior.
*/
@NonNull
public static ListenableFuture<List<ActiveTileIdentifier>> getActiveTilesAsync(
@NonNull Context context, @NonNull Executor executor) {
return getActiveTilesAsync(context, executor, sTimeSourceClock);
}
@VisibleForTesting
@NonNull
static ListenableFuture<List<ActiveTileIdentifier>> getActiveTilesAsync(
@NonNull Context context,
@NonNull Executor executor,
@NonNull TimeSourceClock timeSourceClock) {
return readActiveTilesSharedPref(
getActiveTilesSharedPreferences(context),
context.getPackageName(),
executor,
timeSourceClock);
}
TimeSourceClock getTimeSourceClock() {
return sTimeSourceClock;
}
private TileProvider.Stub mBinder;
@Override
@Nullable
public IBinder onBind(@NonNull Intent intent) {
if (ACTION_BIND_TILE_PROVIDER.equals(intent.getAction())) {
if (mBinder == null) {
mBinder = new TileProviderWrapper(this, new Handler(getMainLooper()));
}
return mBinder;
}
return null;
}
@SuppressWarnings("ExecutorTaskName")
private static class TileProviderWrapper extends TileProvider.Stub {
private final WeakReference<TileService> mServiceRef;
private final Handler mHandler;
TileProviderWrapper(TileService tileService, Handler handler) {
mServiceRef = new WeakReference<>(tileService);
this.mHandler = handler;
}
@Override
public int getApiVersion() {
return TileProvider.API_VERSION;
}
@Override
public void onTileRequest(
int tileId, TileRequestData requestParams, TileCallback callback) {
mHandler.post(
() -> {
TileService tileService = mServiceRef.get();
if (tileService != null) {
if (requestParams.getVersion() != TileRequestData.VERSION_PROTOBUF) {
Log.e(
TAG,
"TileRequestData had unexpected version: "
+ requestParams.getVersion());
return;
}
tileService.markTileAsActive(tileId);
TileRequest tileRequest;
try {
RequestProto.TileRequest tileRequestProto =
RequestProto.TileRequest.parseFrom(
requestParams.getContents());
RequestProto.TileRequest.Builder tileRequestProtoBuilder =
tileRequestProto.toBuilder();
tileRequestProtoBuilder.setTileId(tileId);
// If schema version is missing, go and fill it back in again.
// Explicitly check that device_config is set though. If not, then
// skip entirely.
if (tileRequestProto.hasDeviceConfiguration()
&& !tileRequestProto
.getDeviceConfiguration()
.hasRendererSchemaVersion()) {
DeviceParameters deviceParams =
tileRequestProto.getDeviceConfiguration().toBuilder()
.setRendererSchemaVersion(DEFAULT_VERSION)
.build();
tileRequestProtoBuilder.setDeviceConfiguration(deviceParams);
}
tileRequest =
TileRequest.fromProto(tileRequestProtoBuilder.build());
} catch (InvalidProtocolBufferException ex) {
Log.e(TAG, "Error deserializing TileRequest payload.", ex);
return;
}
ListenableFuture<Tile> tileFuture =
tileService.onTileRequest(tileRequest);
tileFuture.addListener(
() -> {
try {
// Inject the current schema version.
TileProto.Tile tile =
tileFuture.get().toProto().toBuilder()
.setSchemaVersion(Version.CURRENT)
.build();
callback.updateTileData(
new TileData(
tile.toByteArray(),
TileData.VERSION_PROTOBUF));
} catch (ExecutionException
| InterruptedException
| CancellationException ex) {
Log.e(TAG, "onTileRequest Future failed", ex);
} catch (RemoteException ex) {
Log.e(
TAG,
"RemoteException while returning tile payload",
ex);
}
},
mHandler::post);
}
});
}
@Override
@SuppressWarnings("deprecation") // for backward compatibility
public void onResourcesRequest(
int tileId, ResourcesRequestData requestParams, ResourcesCallback callback) {
mHandler.post(
() -> {
TileService tileService = mServiceRef.get();
if (tileService != null) {
if (requestParams.getVersion()
!= ResourcesRequestData.VERSION_PROTOBUF) {
Log.e(
TAG,
"ResourcesRequestData had unexpected version: "
+ requestParams.getVersion());
return;
}
tileService.markTileAsActive(tileId);
ResourcesRequest req;
try {
RequestProto.ResourcesRequest resourcesRequestProto =
RequestProto.ResourcesRequest.parseFrom(
requestParams.getContents());
RequestProto.ResourcesRequest.Builder resourcesRequestProtoBuilder =
resourcesRequestProto.toBuilder();
resourcesRequestProtoBuilder.setTileId(tileId);
if (resourcesRequestProto.hasDeviceConfiguration()
&& !resourcesRequestProto
.getDeviceConfiguration()
.hasRendererSchemaVersion()) {
DeviceParameters deviceParams =
resourcesRequestProto
.getDeviceConfiguration()
.toBuilder()
.setRendererSchemaVersion(DEFAULT_VERSION)
.build();
resourcesRequestProtoBuilder.setDeviceConfiguration(
deviceParams);
}
req =
ResourcesRequest.fromProto(
resourcesRequestProtoBuilder.build());
} catch (InvalidProtocolBufferException ex) {
Log.e(TAG, "Error deserializing ResourcesRequest payload.", ex);
return;
}
ListenableFuture<Resources> resourcesFuture =
tileService.onTileResourcesRequest(req);
if (resourcesFuture.isDone()) {
try {
Resources resources = resourcesFuture.get();
updateResources(callback, resources.toProto().toByteArray());
} catch (ExecutionException
| InterruptedException
| CancellationException ex) {
Log.e(TAG, "onTileResourcesRequest Future failed", ex);
}
} else {
resourcesFuture.addListener(
() -> {
try {
updateResources(
callback,
resourcesFuture
.get()
.toProto()
.toByteArray());
} catch (ExecutionException
| InterruptedException
| CancellationException ex) {
Log.e(
TAG,
"onTileResourcesRequest Future failed",
ex);
}
},
mHandler::post);
}
}
});
}
@Override
public void onTileAddEvent(TileAddEventData data) {
mHandler.post(
() -> {
TileService tileService = mServiceRef.get();
if (tileService != null) {
if (data.getVersion() != TileAddEventData.VERSION_PROTOBUF) {
Log.e(
TAG,
"TileAddEventData had unexpected version: "
+ data.getVersion());
return;
}
try {
TileAddEvent evt =
TileAddEvent.fromProto(
EventProto.TileAddEvent.parseFrom(
data.getContents()));
tileService.markTileAsActive(evt.getTileId());
tileService.onTileAddEvent(evt);
} catch (InvalidProtocolBufferException ex) {
Log.e(TAG, "Error deserializing TileAddEvent payload.", ex);
}
}
});
}
@Override
public void onTileRemoveEvent(TileRemoveEventData data) {
mHandler.post(
() -> {
TileService tileService = mServiceRef.get();
if (tileService != null) {
if (data.getVersion() != TileRemoveEventData.VERSION_PROTOBUF) {
Log.e(
TAG,
"TileRemoveEventData had unexpected version: "
+ data.getVersion());
return;
}
try {
TileRemoveEvent evt =
TileRemoveEvent.fromProto(
EventProto.TileRemoveEvent.parseFrom(
data.getContents()));
tileService.markTileAsInactive(evt.getTileId());
tileService.onTileRemoveEvent(evt);
} catch (InvalidProtocolBufferException ex) {
Log.e(TAG, "Error deserializing TileRemoveEvent payload.", ex);
}
}
});
}
@Override
public void onTileEnterEvent(TileEnterEventData data) {
mHandler.post(
() -> {
TileService tileService = mServiceRef.get();
if (tileService != null) {
if (data.getVersion() != TileEnterEventData.VERSION_PROTOBUF) {
Log.e(
TAG,
"TileEnterEventData had unexpected version: "
+ data.getVersion());
return;
}
try {
TileEnterEvent evt =
TileEnterEvent.fromProto(
EventProto.TileEnterEvent.parseFrom(
data.getContents()));
tileService.markTileAsActive(evt.getTileId());
tileService.onTileEnterEvent(evt);
} catch (InvalidProtocolBufferException ex) {
Log.e(TAG, "Error deserializing TileEnterEvent payload.", ex);
}
}
});
}
@Override
public void onTileLeaveEvent(TileLeaveEventData data) {
mHandler.post(
() -> {
TileService tileService = mServiceRef.get();
if (tileService != null) {
if (data.getVersion() != TileLeaveEventData.VERSION_PROTOBUF) {
Log.e(
TAG,
"TileLeaveEventData had unexpected version: "
+ data.getVersion());
return;
}
try {
TileLeaveEvent evt =
TileLeaveEvent.fromProto(
EventProto.TileLeaveEvent.parseFrom(
data.getContents()));
tileService.markTileAsActive(evt.getTileId());
tileService.onTileLeaveEvent(evt);
} catch (InvalidProtocolBufferException ex) {
Log.e(TAG, "Error deserializing TileLeaveEvent payload.", ex);
}
}
});
}
}
static void updateResources(ResourcesCallback callback, byte[] resources) {
try {
callback.updateResources(new ResourcesData(resources, ResourcesData.VERSION_PROTOBUF));
} catch (RemoteException ex) {
Log.e(TAG, "RemoteException while returning resources payload", ex);
}
}
/**
* Mark tile instance as active by adding it to the {@code ACTIVE_TILES_SHARED_PREF_NAME} shared
* preferences if it doesn't already exist. If the tile instance is already present the
* timestamp is updated if necessary to indicate the tile is still active.
*
* <p>This method is called from {@link TileService#onTileAddEvent}, {@link
* TileService#onTileEnterEvent}, {@link TileService#onTileLeaveEvent}, {@link
* TileService#onTileRequest}, {@link TileService#onTileResourcesRequest} when an interaction
* with the tile is observed, indicating its presence in the carousel.
*/
private void markTileAsActive(int tileId) {
ComponentName componentName = new ComponentName(this, this.getClass().getName());
SharedPreferences sharedPref = getActiveTilesSharedPreferences(this);
cleanupActiveTilesSharedPref(sharedPref, getTimeSourceClock());
String key = new ActiveTileIdentifier(componentName, tileId).flattenToString();
if (sharedPref.contains(key)
&& !timestampNeedsUpdate(sharedPref.getLong(key, -1L), getTimeSourceClock())) {
return;
}
sharedPref.edit().putLong(key, getTimeSourceClock().getCurrentTimestampMillis()).apply();
}
/**
* Mark tile instance as inactive by removing it from the {@code ACTIVE_TILES_SHARED_PREF_NAME}
* shared preferences if it exists.
*
* <p>This method is called from {@link TileService#onTileRemoveEvent} when a tile instance is
* removed from the carousel.
*/
private void markTileAsInactive(int tileId) {
SharedPreferences sharedPref = getActiveTilesSharedPreferences(this);
String key =
new ActiveTileIdentifier(new ComponentName(this, this.getClass().getName()), tileId)
.flattenToString();
if (!sharedPref.contains(key)) {
return;
}
sharedPref.edit().remove(key).apply();
}
/**
* Clean-up method to remove entries with timestamps that haven't been updated for longer than
* {@code INACTIVE_TILE_PERIOD_MS}. In such cases the tiles are considered inactive and will be
* removed from the {@code ACTIVE_TILES_SHARED_PREF_NAME} preferences so that entries are not
* left in the app's storage indefinitely if an {@link TileService#onTileRemoveEvent} callback,
* signaling the tile was removed from the carousel, is missed.
*
* <p>This method is called on any user interactions with the tiles and before the
* SharedPreferences are read.
*/
private static void cleanupActiveTilesSharedPref(
@NonNull SharedPreferences activeTilesSharedPref,
@NonNull TimeSourceClock timeSourceClock) {
StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskReads();
try {
for (String key : activeTilesSharedPref.getAll().keySet()) {
if (isTileInactive(activeTilesSharedPref.getLong(key, -1L), timeSourceClock)) {
activeTilesSharedPref.edit().remove(key).apply();
}
}
} finally {
StrictMode.setThreadPolicy(oldPolicy);
}
}
private static ListenableFuture<List<ActiveTileIdentifier>> readActiveTilesSharedPref(
@NonNull SharedPreferences activeTilesSharedPref,
@NonNull String packageName,
@NonNull Executor executor,
@NonNull TimeSourceClock timeSourceClock) {
return CallbackToFutureAdapter.getFuture(
completer -> {
executor.execute(
() -> {
if (activeTilesSharedPref != null) {
cleanupActiveTilesSharedPref(
activeTilesSharedPref, timeSourceClock);
List<ActiveTileIdentifier> activeTilesList =
activeTilesSharedPref.getAll().entrySet().stream()
.map(
entry ->
ActiveTileIdentifier
.unflattenFromString(
entry.getKey()))
.collect(Collectors.toList());
if (!packageNameMatches(packageName, activeTilesList)) {
completer.setException(
new IllegalArgumentException(
"The information from the provided "
+ "context doesn't match."));
} else {
completer.set(activeTilesList);
}
} else {
completer.setException(
new IllegalArgumentException(
"The information from the provided "
+ "context doesn't match."));
}
});
return "readActiveTilesSharedPref";
});
}
private static SharedPreferences getActiveTilesSharedPreferences(@NonNull Context context) {
StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskReads();
try {
return context.getSharedPreferences(ACTIVE_TILES_SHARED_PREF_NAME, MODE_PRIVATE);
} finally {
StrictMode.setThreadPolicy(oldPolicy);
}
}
/**
* Returns true if the timestamp hasn't been updated for longer than {@code
* UPDATE_TILE_TIMESTAMP_PERIOD_MS}. Returns false if the timestamp has been updated in the past
* {@code UPDATE_TILE_TIMESTAMP_PERIOD_MS} or if the current time cannot be obtained.
*/
private static boolean timestampNeedsUpdate(
long timestampMs, @NonNull TimeSourceClock timeSourceClock) {
return timeSourceClock.getCurrentTimestampMillis() - timestampMs
>= UPDATE_TILE_TIMESTAMP_PERIOD_MS;
}
/**
* Returns true if the timestamp hasn't been updated for longer than {@code
* INACTIVE_TILE_PERIOD_MS}. Returns false if the timestamp has been updated in the past {@code
* INACTIVE_TILE_PERIOD_MS} or if the current time cannot be obtained.
*/
private static boolean isTileInactive(
long timestampMs, @NonNull TimeSourceClock timeSourceClock) {
return timeSourceClock.getCurrentTimestampMillis() - timestampMs >= INACTIVE_TILE_PERIOD_MS;
}
private static boolean packageNameMatches(
String packageName, List<ActiveTileIdentifier> activeTileIdentifiers) {
return activeTileIdentifiers.stream()
.allMatch(i -> i.getComponentName().getPackageName().equals(packageName));
}
interface TimeSourceClock {
/** Returns time agnostic timestamp with the current time. */
long getCurrentTimestampMillis();
}
static class TimeSourceClockImpl implements TimeSourceClock {
@Override
public long getCurrentTimestampMillis() {
return System.currentTimeMillis();
}
}
private static <T> ListenableFuture<T> createFailedFuture(@NonNull Throwable throwable) {
ResolvableFuture<T> errorFuture = ResolvableFuture.create();
errorFuture.setException(throwable);
return errorFuture;
}
}