| /* |
| * Copyright 2020 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.watchface |
| |
| import android.annotation.TargetApi |
| import android.content.BroadcastReceiver |
| import android.content.ComponentName |
| import android.content.Context |
| import android.content.Intent |
| import android.content.IntentFilter |
| import android.graphics.Bitmap |
| import android.graphics.Canvas |
| import android.graphics.Rect |
| import android.icu.util.Calendar |
| import android.os.Bundle |
| import android.os.Handler |
| import android.os.Looper |
| import android.os.PowerManager |
| import android.os.RemoteException |
| import android.os.Trace |
| import android.service.wallpaper.WallpaperService |
| import android.support.wearable.complications.ComplicationData |
| import android.support.wearable.watchface.Constants |
| import android.support.wearable.watchface.IWatchFaceCommand |
| import android.support.wearable.watchface.IWatchFaceService |
| import android.support.wearable.watchface.accessibility.ContentDescriptionLabel |
| import android.support.wearable.watchface.toAshmemCompressedImageBundle |
| import android.util.Log |
| import android.view.Choreographer |
| import android.view.SurfaceHolder |
| import androidx.annotation.IntDef |
| import androidx.wear.complications.SystemProviders.ProviderId |
| import androidx.wear.watchface.data.ImmutableSystemState |
| import androidx.wear.watchface.data.IndicatorState |
| import androidx.wear.watchface.data.SystemState |
| import androidx.wear.watchface.style.StyleUtils |
| import androidx.wear.watchface.style.UserStyleCategory |
| import java.util.concurrent.CountDownLatch |
| |
| /** |
| * After user code finishes, we need up to 100ms of wake lock holding for the drawing to occur. This |
| * isn't the ideal approach, but the framework doesn't expose a callback that would tell us when our |
| * Canvas was drawn. 100 ms should give us time for a few frames to be drawn, in case there is a |
| * backlog. If we encounter issues with this approach, we should consider asking framework team to |
| * expose a callback. |
| */ |
| internal const val SURFACE_DRAW_TIMEOUT_MS = 100L |
| |
| /** |
| * Used to parameterize watch face drawing based on the current system state. |
| * |
| * @hide |
| */ |
| @IntDef( |
| value = [ |
| DrawMode.INTERACTIVE, |
| DrawMode.LOW_BATTERY_INTERACTIVE, |
| DrawMode.MUTE, |
| DrawMode.AMBIENT, |
| DrawMode.BASE_WATCHFACE, |
| DrawMode.UPPER_LAYER |
| ] |
| ) |
| annotation class DrawMode { |
| companion object { |
| /** This mode is used when the user is interacting with the watch face. */ |
| const val INTERACTIVE = 0 |
| |
| /** |
| * This mode is used when the user is interacting with the watch face but the battery is |
| * low, the watch face should render fewer pixels, ideally with darker colors. |
| */ |
| const val LOW_BATTERY_INTERACTIVE = 1 |
| |
| /** |
| * This mode is used when there's an interruption filter. The watch face should look muted. |
| */ |
| const val MUTE = 2 |
| |
| /** |
| * In this mode as few pixels as possible should be turned on, ideally with darker colors. |
| */ |
| const val AMBIENT = 3 |
| |
| /** |
| * As {@link INTERACTIVE} but complications shouldn't be drawn, nor should any watch face |
| * elements that might occlude complications (e.g. watch hands). Used by the |
| * remote configuration UI. |
| */ |
| const val BASE_WATCHFACE = 4 |
| |
| /** |
| * Related to {@link BASE_WATCHFACE}, only watch face elements that might occlude |
| * complications should be drawn (e.g. watch hands). If nothing can occlude the |
| * complications then nothing should be drawn. Used by the remote configuration UI. A screen |
| * shot taken in this mode needs to include an alpha channel. |
| */ |
| const val UPPER_LAYER = 5 |
| |
| fun values(): Collection<Int> = arrayListOf( |
| INTERACTIVE, |
| LOW_BATTERY_INTERACTIVE, |
| MUTE, |
| AMBIENT, |
| BASE_WATCHFACE, |
| UPPER_LAYER |
| ) |
| } |
| } |
| |
| /** @hide */ |
| @IntDef( |
| value = [ |
| TapType.TOUCH, |
| TapType.TOUCH_CANCEL, |
| TapType.TAP |
| ] |
| ) |
| annotation class TapType { |
| companion object { |
| /** Used in onTapCommand to indicate a "down" touch event on the watch face. */ |
| const val TOUCH = 0 |
| |
| /** |
| * Used in onTapCommand to indicate that a previous TapType.TOUCH touch event has been |
| * canceled. This generally happens when the watch face is touched but then a move or long |
| * press occurs. |
| */ |
| const val TOUCH_CANCEL = 1 |
| |
| /** |
| * Used in onTapCommaned to indicate that an "up" event on the watch face has occurred that |
| * has not been consumed by another activity. A TapType.TOUCH will always occur first. |
| * This event will not occur if a TapType.TOUCH_CANCEL is sent. |
| */ |
| const val TAP = 2 |
| } |
| } |
| |
| /** |
| * WatchFaceService and {@link WatchFace} are a pair of base classes intended to handle much of |
| * the boilerplate needed to implement a watch face without being too opinionated. The suggested |
| * structure of an WatchFaceService based watch face is: |
| * |
| * @sample androidx.wear.watchface.samples.kDocCreateExampleWatchFaceService |
| * |
| * Base classes for complications and styles are provided along with a default UI for configuring |
| * them. Complications are optional, however if required, WatchFaceService assumes all |
| * complications can be enumerated up front and passed as a collection into WatchFace's constructor. |
| * Some watch faces support different configurations (number & position) of complications and this |
| * can be achieved by rendering a subset and only marking the ones you need as active. Most watch |
| * faces will not animate the location of complications so its recommended to use the |
| * WatchFace.UnitSquareBoundsProvider helper. |
| * |
| * Many watch faces support styles, typically controlling the color and visual look of watch face |
| * elements such as numeric fonts, watch hands and ticks. WatchFaceService doesn't take an |
| * an opinion on what comprises a style beyond it should be representable as a map of categories to |
| * options. |
| * |
| * It's recommended to avoid putting business logic in sub classes of WatchFaceService, rather |
| * that should go in sub classes of WatchFace. |
| * |
| * To aid debugging watch face animations, WatchFaceService allows you to speed up or slow down |
| * time, and to loop between two instants. This is controlled by MOCK_TIME_INTENT intents |
| * with a float extra called "androidx.wear.watchface.extra.MOCK_TIME_SPEED_MULTIPLIE" and to long |
| * extras called "androidx.wear.watchface.extra.MOCK_TIME_WRAPPING_MIN_TIME" and |
| * "androidx.wear.watchface.extra.MOCK_TIME_WRAPPING_MAX_TIME" (which are UTC time in milliseconds). |
| * If minTime is omitted or set to -1 then the current time is sampled as minTime. |
| * |
| * E.g, to make time go twice as fast: |
| * adb shell am broadcast -a androidx.wear.watchface.MockTime \ |
| * --ef androidx.wear.watchface.extra.MOCK_TIME_SPEED_MULTIPLIER 2.0 |
| * |
| * |
| * To use the default watch face configuration UI add the following into your watch face's |
| * AndroidManifest.xml: |
| * |
| * ``` |
| * <activity |
| * android:name="androidx.wear.watchface.ui.WatchFaceConfigActivity" |
| * android:exported="true" |
| * android:label="Config" |
| * android:theme="@android:style/Theme.Translucent.NoTitleBar"> |
| * <intent-filter> |
| * <action android:name="com.google.android.clockwork.watchfaces.complication.CONFIG_DIGITAL" /> |
| * <category android:name= |
| * "com.google.android.wearable.watchface.category.WEARABLE_CONFIGURATION" /> |
| * <category android:name="android.intent.category.DEFAULT" /> |
| * </intent-filter> |
| * </activity> |
| * ``` |
| * |
| * To register a WatchFaceService with the system add a <service> tag to the <application> in your |
| * watch face's AndroidManifest.xml: |
| * |
| * ``` |
| * <service |
| * android:name=".MyWatchFaceServiceClass" |
| * android:exported="true" |
| * android:label="@string/watch_face_name" |
| * android:permission="android.permission.BIND_WALLPAPER"> |
| * <intent-filter> |
| * <action android:name="android.service.wallpaper.WallpaperService" /> |
| * <category android:name="com.google.android.wearable.watchface.category.WATCH_FACE" /> |
| * </intent-filter> |
| * <meta-data |
| * android:name="com.google.android.wearable.watchface.preview" |
| * android:resource="@drawable/my_watch_preview" /> |
| * <meta-data |
| * android:name="com.google.android.wearable.watchface.preview_circular" |
| * android:resource="@drawable/my_watch_circular_preview" /> |
| * <meta-data |
| * android:name="com.google.android.wearable.watchface.wearableConfigurationAction" |
| * android:value="com.google.android.clockwork.watchfaces.complication.CONFIG_DIGITAL"/> |
| * <meta-data |
| * android:name="android.service.wallpaper" |
| * android:resource="@xml/watch_face" /> |
| * </service> |
| * ``` |
| * |
| * Multiple watch faces can be defined in the same package, requiring multiple <service> tags. |
| */ |
| @TargetApi(26) |
| abstract class WatchFaceService : WallpaperService() { |
| |
| private companion object { |
| private const val TAG = "WatchFaceService" |
| |
| /** Whether to log every frame. */ |
| private const val LOG_VERBOSE = false |
| |
| /** Whether to enable tracing for each call to {@link Engine#onDraw}. */ |
| private const val TRACE_DRAW = false |
| } |
| |
| /** Override this factory method to create your WatchFace. */ |
| protected abstract fun createWatchFace( |
| surfaceHolder: SurfaceHolder, |
| watchFaceHost: WatchFaceHost, |
| watchState: WatchState |
| ): WatchFace |
| |
| final override fun onCreateEngine() = EngineWrapper(getHandler()) as Engine |
| |
| // This is open to allow mocking. |
| internal open fun getHandler() = Handler(Looper.getMainLooper()) |
| |
| // This is open to allow mocking. |
| internal open fun getMutableWatchState() = MutableWatchState() |
| |
| internal inner class EngineWrapper( |
| private val _handler: Handler |
| ) : WallpaperService.Engine(), WatchFaceHostApi { |
| private val _context = this@WatchFaceService as Context |
| |
| private lateinit var currentSurfaceHolder: SurfaceHolder |
| private var currentSurfaceFormat = 0 |
| private var currentSurfaceWidth = 0 |
| private var currentSurfaceHeight = 0 |
| |
| internal lateinit var iWatchFaceService: IWatchFaceService |
| internal lateinit var watchFace: WatchFace |
| |
| internal val mutableWatchState = getMutableWatchState().apply { |
| isVisible.value = this@EngineWrapper.isVisible |
| } |
| |
| private var timeTickRegistered = false |
| private val timeTickReceiver: BroadcastReceiver = object : BroadcastReceiver() { |
| @SuppressWarnings("SyntheticAccessor") |
| override fun onReceive(context: Context, intent: Intent) { |
| watchFace.invalidate() |
| } |
| } |
| |
| private var destroyed = false |
| |
| internal lateinit var ambientUpdateWakelock: PowerManager.WakeLock |
| |
| private val choreographer = Choreographer.getInstance() |
| |
| /** |
| * Whether we already have a {@link #frameCallback} posted and waiting in the {@link |
| * Choreographer} queue. This protects us from drawing multiple times in a single frame. |
| */ |
| private var frameCallbackPending = false |
| |
| private val frameCallback = object : Choreographer.FrameCallback { |
| @SuppressWarnings("SyntheticAccessor") |
| override fun doFrame(frameTimeNs: Long) { |
| if (destroyed) { |
| return |
| } |
| frameCallbackPending = false |
| draw() |
| } |
| } |
| |
| private val invalidateRunnable = Runnable(this::invalidate) |
| |
| private val ambientTimeTickFilter = IntentFilter().apply { |
| addAction(Intent.ACTION_DATE_CHANGED) |
| addAction(Intent.ACTION_TIME_CHANGED) |
| addAction(Intent.ACTION_TIMEZONE_CHANGED) |
| } |
| |
| private val interactiveTimeTickFilter = IntentFilter(ambientTimeTickFilter).apply { |
| addAction(Intent.ACTION_TIME_TICK) |
| } |
| |
| /** |
| * Runs the supplied task on the UI thread. If we're not on the UI thread a task is posted |
| * and we block until it's been processed. |
| * |
| * AIDL calls are dispatched from a thread pool, but for simplicity WatchFace code is |
| * largely single threaded so we need to post tasks to the UI thread and wait for them to |
| * execute. |
| */ |
| internal fun <R> runOnUiThread(task: () -> R) = |
| if (_handler.looper == Looper.myLooper()) { |
| task.invoke() |
| } else { |
| val latch = CountDownLatch(1) |
| var returnVal: R? = null |
| var exception: Exception? = null |
| if (_handler.post { |
| try { |
| returnVal = task.invoke() |
| } catch (e: Exception) { |
| // Will rethrow on the calling thread. |
| exception = e |
| } |
| latch.countDown() |
| }) { |
| latch.await() |
| if (exception != null) { |
| throw exception as Exception |
| } |
| } |
| returnVal!! |
| } |
| |
| private val watchFaceCommand = object : IWatchFaceCommand.Stub() { |
| override fun getApiVersion() = IWatchFaceCommand.WATCHFACE_COMMAND_API_VERSION |
| |
| override fun ambientUpdate() { |
| runOnUiThread { |
| if (mutableWatchState.isAmbient.value) { |
| ambientUpdateWakelock.acquire() |
| watchFace.invalidate() |
| ambientUpdateWakelock.acquire(SURFACE_DRAW_TIMEOUT_MS) |
| } |
| } |
| } |
| |
| override fun setSystemState(systemState: SystemState) { |
| runOnUiThread { |
| if (firstSetSystemState || |
| systemState.inAmbientMode != mutableWatchState.isAmbient.value |
| ) { |
| mutableWatchState.isAmbient.value = systemState.inAmbientMode |
| updateTimeTickReceiver() |
| } |
| |
| if (firstSetSystemState || |
| systemState.interruptionFilter != mutableWatchState.interruptionFilter.value |
| ) { |
| mutableWatchState.interruptionFilter.value = systemState.interruptionFilter |
| } |
| |
| if (firstSetSystemState || |
| systemState.unreadCount != mutableWatchState.unreadNotificationCount.value |
| ) { |
| mutableWatchState.unreadNotificationCount.value = systemState.unreadCount |
| } |
| |
| if (firstSetSystemState || |
| systemState.notificationCount != mutableWatchState.notificationCount.value |
| ) { |
| mutableWatchState.notificationCount.value = systemState.notificationCount |
| } |
| |
| firstSetSystemState = false |
| } |
| } |
| |
| override fun setIndicatorState(indicatorState: IndicatorState) { |
| runOnUiThread { |
| if (firstIndicatorState || |
| indicatorState.isCharging != mutableWatchState.isCharging.value |
| ) { |
| mutableWatchState.isCharging.value = indicatorState.isCharging |
| } |
| |
| if (firstIndicatorState || |
| indicatorState.inAirplaneMode != mutableWatchState.inAirplaneMode.value |
| ) { |
| mutableWatchState.inAirplaneMode.value = indicatorState.inAirplaneMode |
| } |
| |
| if (firstIndicatorState || |
| indicatorState.isConnectedToCompanion != |
| mutableWatchState.isConnectedToCompanion.value |
| ) { |
| mutableWatchState.isConnectedToCompanion.value = |
| indicatorState.isConnectedToCompanion |
| } |
| |
| if (firstIndicatorState || |
| indicatorState.inTheaterMode != mutableWatchState.isInTheaterMode.value |
| ) { |
| mutableWatchState.isInTheaterMode.value = indicatorState.inTheaterMode |
| } |
| |
| if (firstIndicatorState || |
| indicatorState.isGpsActive != mutableWatchState.isGpsActive.value |
| ) { |
| mutableWatchState.isGpsActive.value = indicatorState.isGpsActive |
| } |
| |
| if (firstIndicatorState || |
| indicatorState.isKeyguardLocked != mutableWatchState.isKeyguardLocked.value |
| ) { |
| mutableWatchState.isKeyguardLocked.value = indicatorState.isKeyguardLocked |
| } |
| |
| firstIndicatorState = false |
| } |
| } |
| |
| override fun setUserStyle(userStyle: Bundle) { |
| runOnUiThread { |
| watchFace.onSetStyleInternal( |
| StyleUtils.bundleToStyleMap( |
| userStyle, |
| watchFace.userStyleRepository.userStyleCategories |
| ) |
| ) |
| } |
| } |
| |
| override fun setImmutableSystemState(immutableSystemState: ImmutableSystemState) { |
| runOnUiThread { |
| // These properties never change so set them once only. |
| if (!immutableSystemStateDone) { |
| mutableWatchState.hasLowBitAmbient.value = |
| immutableSystemState.hasLowBitAmbient |
| mutableWatchState.hasBurnInProtection.value = |
| immutableSystemState.hasBurnInProtection |
| |
| immutableSystemStateDone = true |
| } |
| } |
| } |
| |
| override fun setComplicationData(complicationId: Int, data: ComplicationData) { |
| runOnUiThread { |
| watchFace.onComplicationDataUpdate(complicationId, data) |
| } |
| } |
| |
| override fun requestWatchFaceStyle() { |
| runOnUiThread { |
| try { |
| iWatchFaceService.setStyle(watchFace.watchFaceStyle) |
| } catch (e: RemoteException) { |
| Log.e(TAG, "Failed to set WatchFaceStyle: ", e) |
| } |
| |
| val activeComplications = lastActiveComplications |
| if (activeComplications != null) { |
| setActiveComplications(activeComplications) |
| } |
| |
| val a11yLabels = lastA11yLabels |
| if (a11yLabels != null) { |
| setContentDescriptionLabels(a11yLabels) |
| } |
| } |
| } |
| |
| override fun takeWatchfaceScreenshot( |
| drawMode: Int, |
| compressionQuality: Int, |
| calendarTimeMillis: Long, |
| userStyle: Bundle? |
| ): Bundle { |
| return runOnUiThread { |
| val oldStyle = HashMap(watchFace.userStyleRepository.userStyle) |
| if (userStyle != null) { |
| watchFace.onSetStyleInternal( |
| StyleUtils.bundleToStyleMap( |
| userStyle, |
| watchFace.userStyleRepository.userStyleCategories |
| ) |
| ) |
| } |
| |
| val bitmap = watchFace.renderer.takeScreenshot( |
| Calendar.getInstance().apply { |
| timeInMillis = calendarTimeMillis |
| }, |
| drawMode |
| ) |
| |
| // Restore previous style if required. |
| if (userStyle != null) { |
| watchFace.onSetStyleInternal(oldStyle) |
| } |
| |
| bitmap |
| }.toAshmemCompressedImageBundle( |
| compressionQuality |
| ) |
| } |
| |
| override fun takeComplicationScreenshot( |
| complicationId: Int, |
| drawMode: Int, |
| compressionQuality: Int, |
| calendarTimeMillis: Long, |
| complicationData: ComplicationData?, |
| userStyle: Bundle? |
| ): Bundle? { |
| return runOnUiThread { |
| val calendar = Calendar.getInstance().apply { |
| timeInMillis = calendarTimeMillis |
| } |
| val complication = watchFace.complicationsManager[complicationId] |
| if (complication != null) { |
| val oldStyle = HashMap(watchFace.userStyleRepository.userStyle) |
| if (userStyle != null) { |
| watchFace.onSetStyleInternal( |
| StyleUtils.bundleToStyleMap( |
| userStyle, |
| watchFace.userStyleRepository.userStyleCategories |
| ) |
| ) |
| } |
| |
| val bounds = complication.computeBounds(watchFace.renderer.screenBounds) |
| val complicationBitmap = |
| Bitmap.createBitmap( |
| bounds.width(), bounds.height(), |
| Bitmap.Config.ARGB_8888 |
| ) |
| |
| var prevComplicationData: ComplicationData? = null |
| if (complicationData != null) { |
| prevComplicationData = complication.renderer.getData() |
| complication.renderer.setData(complicationData) |
| } |
| |
| complication.renderer.onDraw( |
| Canvas(complicationBitmap), |
| Rect(0, 0, bounds.width(), bounds.height()), |
| calendar, |
| drawMode |
| ) |
| |
| // Restore previous ComplicationData & style if required. |
| if (complicationData != null) { |
| complication.renderer.setData(prevComplicationData) |
| } |
| |
| if (userStyle != null) { |
| watchFace.onSetStyleInternal(oldStyle) |
| } |
| |
| complicationBitmap.toAshmemCompressedImageBundle( |
| compressionQuality |
| ) |
| } else { |
| null |
| } |
| } |
| } |
| |
| override fun sendTouchEvent(xPos: Int, yPos: Int, tapType: Int) { |
| runOnUiThread { |
| if (watchFaceCreated()) { |
| watchFace.onTapCommand(tapType, xPos, yPos) |
| } |
| } |
| } |
| } |
| |
| // Only valid after onSetBinder has been called. |
| private var systemApiVersion = -1 |
| |
| internal var firstSetSystemState = true |
| internal var firstIndicatorState = true |
| internal var immutableSystemStateDone = false |
| |
| internal var lastActiveComplications: IntArray? = null |
| internal var lastA11yLabels: Array<ContentDescriptionLabel>? = null |
| |
| private var pendingBackgroundAction: Bundle? = null |
| private var pendingProperties: Bundle? = null |
| private var pendingSetWatchFaceStyle = false |
| private var pendingVisibilityChanged: Boolean? = null |
| private var pendingComplicationDataUpdates = ArrayList<Bundle>() |
| private var complicationsActivated = false |
| |
| override fun getContext() = _context |
| |
| override fun getHandler() = _handler |
| |
| override fun onCreate(holder: SurfaceHolder) { |
| super.onCreate(holder) |
| |
| ambientUpdateWakelock = |
| (getSystemService(Context.POWER_SERVICE) as PowerManager) |
| .newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "$TAG:[AmbientUpdate]") |
| // Disable reference counting for our wake lock so that we can use the same wake lock |
| // for user code in invaliate() and after that for having canvas drawn. |
| ambientUpdateWakelock.setReferenceCounted(false) |
| } |
| |
| override fun onDestroy() { |
| destroyed = true |
| _handler.removeCallbacks(invalidateRunnable) |
| choreographer.removeFrameCallback(frameCallback) |
| |
| if (timeTickRegistered) { |
| timeTickRegistered = false |
| unregisterReceiver(timeTickReceiver) |
| } |
| |
| if (!watchFaceCreated()) { |
| watchFace.onDestroy() |
| } |
| super.onDestroy() |
| } |
| |
| override fun onCommand( |
| action: String?, |
| x: Int, |
| y: Int, |
| z: Int, |
| extras: Bundle?, |
| resultRequested: Boolean |
| ): Bundle? { |
| when (action) { |
| Constants.COMMAND_AMBIENT_UPDATE -> watchFaceCommand.ambientUpdate() |
| Constants.COMMAND_BACKGROUND_ACTION -> onBackgroundAction(extras!!) |
| Constants.COMMAND_COMPLICATION_DATA -> onComplicationDataUpdate(extras!!) |
| Constants.COMMAND_REQUEST_STYLE -> onRequestStyle() |
| Constants.COMMAND_SET_BINDER -> onSetBinder(extras!!) |
| Constants.COMMAND_SET_PROPERTIES -> onPropertiesChanged(extras!!) |
| Constants.COMMAND_SET_USER_STYLE -> watchFaceCommand.setUserStyle(extras!!) |
| Constants.COMMAND_TAP -> watchFaceCommand.sendTouchEvent(x, y, TapType.TAP) |
| Constants.COMMAND_TOUCH -> watchFaceCommand.sendTouchEvent(x, y, TapType.TOUCH) |
| Constants.COMMAND_TOUCH_CANCEL -> watchFaceCommand.sendTouchEvent( |
| x, |
| y, |
| TapType.TOUCH_CANCEL |
| ) |
| else -> { |
| } |
| } |
| return null |
| } |
| |
| fun onBackgroundAction(extras: Bundle) { |
| // We can't guarantee the binder has been set and onSurfaceChanged called before this |
| // command. |
| if (!watchFaceCreated()) { |
| pendingBackgroundAction = extras |
| return |
| } |
| |
| watchFaceCommand.setSystemState( |
| SystemState( |
| extras.getBoolean( |
| Constants.EXTRA_AMBIENT_MODE, |
| mutableWatchState.isAmbient.getValueOr(false) |
| ), |
| extras.getInt( |
| Constants.EXTRA_INTERRUPTION_FILTER, |
| mutableWatchState.interruptionFilter.getValueOr(0) |
| ), |
| extras.getInt( |
| Constants.EXTRA_UNREAD_COUNT, |
| mutableWatchState.unreadNotificationCount.getValueOr(0) |
| ), |
| extras.getInt( |
| Constants.EXTRA_NOTIFICATION_COUNT, |
| mutableWatchState.notificationCount.getValueOr(0) |
| ) |
| ) |
| ) |
| |
| val statusBundle = extras.getBundle(Constants.EXTRA_INDICATOR_STATUS) |
| if (statusBundle != null) { |
| watchFaceCommand.setIndicatorState( |
| IndicatorState( |
| statusBundle.getBoolean(Constants.STATUS_CHARGING), |
| statusBundle.getBoolean(Constants.STATUS_AIRPLANE_MODE), |
| statusBundle.getBoolean(Constants.STATUS_CONNECTED), |
| statusBundle.getBoolean(Constants.STATUS_THEATER_MODE), |
| statusBundle.getBoolean(Constants.STATUS_GPS_ACTIVE), |
| statusBundle.getBoolean(Constants.STATUS_KEYGUARD_LOCKED) |
| ) |
| ) |
| } |
| |
| pendingBackgroundAction = null |
| } |
| |
| private fun onSetBinder(extras: Bundle) { |
| val binder = extras.getBinder(Constants.EXTRA_BINDER) |
| if (binder == null) { |
| Log.w(TAG, "Binder is null.") |
| return |
| } |
| |
| iWatchFaceService = IWatchFaceService.Stub.asInterface(binder) |
| |
| try { |
| // Note if the implementation doesn't support getVersion this will return zero |
| // rather than throwing an exception. |
| systemApiVersion = iWatchFaceService.apiVersion |
| } catch (e: RemoteException) { |
| Log.w(TAG, "Failed to getVersion: ", e) |
| } |
| |
| if (systemApiVersion >= 3) { |
| val bundle = Bundle().apply { |
| putBinder( |
| Constants.EXTRA_WATCH_FACE_COMMAND_BINDER, |
| watchFaceCommand.asBinder() |
| ) |
| } |
| iWatchFaceService.registerIWatchFaceCommand(bundle) |
| } |
| |
| maybeCreateWatchFace() |
| } |
| |
| override fun onSurfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) { |
| currentSurfaceHolder = holder |
| currentSurfaceFormat = format |
| currentSurfaceWidth = width |
| currentSurfaceHeight = height |
| |
| if (watchFaceCreated()) { |
| watchFace.onSurfaceChanged(holder, format, width, height) |
| } else { |
| maybeCreateWatchFace() |
| } |
| } |
| |
| override fun onSurfaceRedrawNeeded(holder: SurfaceHolder) { |
| if (watchFaceCreated()) { |
| watchFace.onSurfaceRedrawNeeded() |
| } |
| } |
| |
| override fun onSurfaceDestroyed(holder: SurfaceHolder) { |
| if (watchFaceCreated()) { |
| watchFace.renderer.onSurfaceDestroyed(holder) |
| } |
| } |
| |
| private fun maybeCreateWatchFace() { |
| // To simplify handling of watch face state, we only construct the {@link WatchFace} |
| // once both currentSurfaceHolder and iWatchFaceService have been initialized. |
| if (this::currentSurfaceHolder.isInitialized && |
| this::iWatchFaceService.isInitialized && !watchFaceCreated() |
| ) { |
| val host = WatchFaceHost() |
| host.api = this |
| watchFace = createWatchFace( |
| currentSurfaceHolder, |
| host, |
| mutableWatchState.asWatchState() |
| ) |
| |
| // Watchfaces especially OpenGL ones often do initialization in |
| // onSurfaceChanged, make sure we send the initial one. |
| watchFace.renderer.onSurfaceChanged( |
| currentSurfaceHolder, |
| currentSurfaceFormat, |
| currentSurfaceWidth, |
| currentSurfaceHeight |
| ) |
| |
| val backgroundAction = pendingBackgroundAction |
| if (backgroundAction != null) { |
| onBackgroundAction(backgroundAction) |
| pendingBackgroundAction = null |
| } |
| if (pendingSetWatchFaceStyle) { |
| onRequestStyle() |
| } |
| val visibility = pendingVisibilityChanged |
| if (visibility != null) { |
| onVisibilityChanged(visibility) |
| pendingVisibilityChanged = null |
| } |
| val properties = pendingProperties |
| if (properties != null) { |
| onPropertiesChanged(properties) |
| pendingProperties = null |
| } |
| for (complicationDataUpdate in pendingComplicationDataUpdates) { |
| onComplicationDataUpdate(complicationDataUpdate) |
| } |
| } |
| } |
| |
| private fun onRequestStyle() { |
| // We can't guarantee the binder has been set and onSurfaceChanged called before this |
| // command. |
| if (!watchFaceCreated()) { |
| pendingSetWatchFaceStyle = true |
| return |
| } |
| watchFaceCommand.requestWatchFaceStyle() |
| pendingSetWatchFaceStyle = false |
| } |
| |
| /** |
| * Registers {@link #timeTickReceiver} if it should be registered and isn't currently, or |
| * unregisters it if it shouldn't be registered but currently is. It also applies the right |
| * intent filter depending on whether we are in ambient mode or not. |
| */ |
| internal fun updateTimeTickReceiver() { |
| // Separate calls are issued to deliver the state of isAmbient and isVisible, so during |
| // init we might not yet know the state of both. |
| if (!mutableWatchState.isAmbient.hasValue() || |
| !mutableWatchState.isVisible.hasValue() |
| ) { |
| return |
| } |
| |
| if (timeTickRegistered) { |
| unregisterReceiver(timeTickReceiver) |
| timeTickRegistered = false |
| } |
| |
| // We only register if we are visible, otherwise it doesn't make sense to waste cycles. |
| if (mutableWatchState.isVisible.value) { |
| if (mutableWatchState.isAmbient.value) { |
| registerReceiver(timeTickReceiver, ambientTimeTickFilter) |
| } else { |
| registerReceiver(timeTickReceiver, interactiveTimeTickFilter) |
| } |
| timeTickRegistered = true |
| |
| // In case we missed a tick while transitioning from ambient to interactive, we |
| // want to make sure the watch face doesn't show stale time when in interactive |
| // mode. |
| watchFace.invalidate() |
| } |
| } |
| |
| override fun onVisibilityChanged(visible: Boolean) { |
| super.onVisibilityChanged(visible) |
| |
| // We are requesting state every time the watch face changes its visibility because |
| // wallpaper commands have a tendency to be dropped. By requesting it on every |
| // visibility change, we ensure that we don't become a victim of some race condition. |
| sendBroadcast( |
| Intent(Constants.ACTION_REQUEST_STATE).apply { |
| putExtra(Constants.EXTRA_WATCH_FACE_VISIBLE, visible) |
| } |
| ) |
| |
| // We can't guarantee the binder has been set and onSurfaceChanged called before this |
| // command. |
| if (!watchFaceCreated()) { |
| pendingVisibilityChanged = visible |
| return |
| } |
| |
| mutableWatchState.isVisible.value = visible |
| updateTimeTickReceiver() |
| pendingVisibilityChanged = null |
| } |
| |
| override fun invalidate() { |
| if (!frameCallbackPending) { |
| if (LOG_VERBOSE) { |
| Log.v(TAG, "invalidate: requesting draw") |
| } |
| frameCallbackPending = true |
| choreographer.postFrameCallback(frameCallback) |
| } else { |
| if (LOG_VERBOSE) { |
| Log.v(TAG, "invalidate: draw already requested") |
| } |
| } |
| } |
| |
| internal fun draw() { |
| try { |
| if (TRACE_DRAW) { |
| Trace.beginSection("onDraw") |
| } |
| if (LOG_VERBOSE) { |
| Log.v(WatchFaceService.TAG, "drawing frame") |
| } |
| watchFace.onDraw() |
| } finally { |
| if (TRACE_DRAW) { |
| Trace.endSection() |
| } |
| } |
| } |
| |
| private fun onComplicationDataUpdate(extras: Bundle) { |
| if (!watchFaceCreated()) { |
| pendingComplicationDataUpdates.add(extras) |
| return |
| } |
| extras.classLoader = ComplicationData::class.java.classLoader |
| watchFaceCommand.setComplicationData( |
| extras.getInt(Constants.EXTRA_COMPLICATION_ID), |
| (extras.getParcelable(Constants.EXTRA_COMPLICATION_DATA) as ComplicationData?)!! |
| ) |
| } |
| |
| internal fun onPropertiesChanged(properties: Bundle) { |
| if (!watchFaceCreated()) { |
| pendingProperties = properties |
| return |
| } |
| |
| watchFaceCommand.setImmutableSystemState( |
| ImmutableSystemState( |
| properties.getBoolean(Constants.PROPERTY_LOW_BIT_AMBIENT), |
| properties.getBoolean(Constants.PROPERTY_BURN_IN_PROTECTION) |
| ) |
| ) |
| } |
| |
| internal fun watchFaceCreated() = this::watchFace.isInitialized |
| |
| override fun setDefaultComplicationProviderWithFallbacks( |
| watchFaceComplicationId: Int, |
| providers: List<ComponentName>?, |
| @ProviderId fallbackSystemProvider: Int, |
| type: Int |
| ) { |
| if (systemApiVersion >= 2) { |
| iWatchFaceService.setDefaultComplicationProviderWithFallbacks( |
| watchFaceComplicationId, |
| providers, |
| fallbackSystemProvider, |
| type |
| ) |
| } else { |
| // If the implementation doesn't support the new API we emulate its behavior by |
| // setting complication providers in the reverse order. This works because if |
| // setDefaultComplicationProvider attempts to set a non-existent or incompatible |
| // provider it does nothing, which allows us to emulate the same semantics as |
| // setDefaultComplicationProviderWithFallbacks albeit with more calls. |
| if (fallbackSystemProvider != WatchFace.NO_DEFAULT_PROVIDER) { |
| iWatchFaceService.setDefaultSystemComplicationProvider( |
| watchFaceComplicationId, fallbackSystemProvider, type |
| ) |
| } |
| |
| if (providers != null) { |
| // Iterate in reverse order. This could be O(n^2) but n is expected to be small |
| // and the list is probably an ArrayList so it's probably O(n) in practice. |
| for (i in providers.size - 1 downTo 0) { |
| iWatchFaceService.setDefaultComplicationProvider( |
| watchFaceComplicationId, providers[i], type |
| ) |
| } |
| } |
| } |
| } |
| |
| override fun setActiveComplications(watchFaceComplicationIds: IntArray) { |
| lastActiveComplications = watchFaceComplicationIds |
| try { |
| iWatchFaceService.setActiveComplications( |
| watchFaceComplicationIds, /* updateAll= */ !complicationsActivated |
| ) |
| complicationsActivated = true |
| } catch (e: RemoteException) { |
| Log.e(TAG, "Failed to set active complications: ", e) |
| } |
| } |
| |
| override fun setContentDescriptionLabels(labels: Array<ContentDescriptionLabel>) { |
| lastA11yLabels = labels |
| try { |
| iWatchFaceService.setContentDescriptionLabels(labels) |
| } catch (e: RemoteException) { |
| Log.e(TAG, "Failed to set accessibility labels: ", e) |
| } |
| } |
| |
| override fun registerWatchFaceType(@WatchFaceType watchFaceType: Int) { |
| if (systemApiVersion >= 3) { |
| iWatchFaceService.registerWatchFaceType(watchFaceType) |
| } |
| } |
| |
| override fun registerUserStyleSchema(styleSchema: List<UserStyleCategory>) { |
| if (systemApiVersion >= 3) { |
| iWatchFaceService.registerUserStyleSchema( |
| StyleUtils.userStyleCategoriesToBundles(styleSchema) |
| ) |
| } |
| } |
| |
| override fun setCurrentUserStyle( |
| userStyle: Map<UserStyleCategory, UserStyleCategory.Option> |
| ) { |
| if (systemApiVersion >= 3) { |
| iWatchFaceService.setCurrentUserStyle( |
| StyleUtils.styleMapToBundle(userStyle) |
| ) |
| } |
| } |
| |
| override fun getStoredUserStyle( |
| schema: List<UserStyleCategory> |
| ): Map<UserStyleCategory, UserStyleCategory.Option>? { |
| if (systemApiVersion < 3) { |
| return null |
| } |
| return StyleUtils.bundleToStyleMap( |
| iWatchFaceService.storedUserStyle ?: Bundle(), |
| schema |
| ) |
| } |
| |
| override fun setComplicationDetails( |
| complicationId: Int, |
| bounds: Rect, |
| @ComplicationBoundsType type: Int |
| ) { |
| if (systemApiVersion >= 3) { |
| iWatchFaceService.setComplicationDetails(complicationId, bounds, type) |
| } |
| } |
| |
| override fun setComplicationSupportedTypes(complicationId: Int, types: IntArray) { |
| if (systemApiVersion >= 3) { |
| iWatchFaceService.setComplicationSupportedTypes(complicationId, types) |
| } |
| } |
| } |
| } |