錶盤可以通過setDefaultSystemComplicationProvider(int watchFaceComplicationId, int systemProvider, int type)
來設置要顯示的系統複雜數據。
一.系統支持哪些複雜數據
SystemProviders列舉了目前系統支持的複雜數據。
package android.support.wearable.complications;
public class SystemProviders {
public static final int WATCH_BATTERY = 1;
public static final int DATE = 2;
public static final int TIME_AND_DATE = 3;
public static final int STEP_COUNT = 4;
public static final int WORLD_CLOCK = 5;
public static final int APP_SHORTCUT = 6;
public static final int UNREAD_NOTIFICATION_COUNT = 7;
public static final int ANDROID_PAY = 8;
public static final int NEXT_EVENT = 9;
public static final int RETAIL_STEP_COUNT = 10;
public static final int RETAIL_CHAT = 11;
public static final int SUNRISE_SUNSET = 12;
public static final int DAY_OF_WEEK = 13;
public static final int FAVORITE_CONTACT = 14;
public static final int MOST_RECENT_APP = 15;
public static final int DAY_AND_DATE = 16;
private static final String HOME_PACKAGE_NAME = "com.google.android.wearable.app";
private static final String BATTERY_CLASS_NAME = "com.google.android.clockwork.home.complications.providers.BatteryProviderService";
private static final String DATE_CLASS_NAME = "com.google.android.clockwork.home.complications.providers.DayOfMonthProviderService";
private static final String CURRENT_TIME_CLASS_NAME = "com.google.android.clockwork.home.complications.providers.CurrentTimeProvider";
private static final String STEPS_CLASS_NAME = "com.google.android.clockwork.home.complications.providers.StepsProviderService";
private static final String NEXT_EVENT_CLASS_NAME = "com.google.android.clockwork.home.complications.providers.NextEventProviderService";
private static final String WORLD_CLOCK_CLASS_NAME = "com.google.android.clockwork.home.complications.providers.WorldClockProviderService";
private static final String APPS_CLASS_NAME = "com.google.android.clockwork.home.complications.providers.LauncherProviderService";
private static final String UNREAD_CLASS_NAME = "com.google.android.clockwork.home.complications.providers.UnreadNotificationsProviderService";
private static final String RETAIL_STEPS_CLASS_NAME = "com.google.android.clockwork.home.complications.providers.RetailStepsProviderService";
private static final String RETAIL_CHAT_CLASS_NAME = "com.google.android.clockwork.home.complications.providers.RetailChatProviderService";
private static final String PAY_PACKAGE_NAME = "com.google.android.apps.walletnfcrel";
private static final String PAY_CLASS_NAME = "com.google.commerce.tapandpay.android.wearable.complications.PayProviderService";
public SystemProviders() {
}
/** @deprecated */
@Deprecated
public static ComponentName batteryProvider() {
return new ComponentName("com.google.android.wearable.app", "com.google.android.clockwork.home.complications.providers.BatteryProviderService");
}
/** @deprecated */
@Deprecated
public static ComponentName dateProvider() {
return new ComponentName("com.google.android.wearable.app", "com.google.android.clockwork.home.complications.providers.DayOfMonthProviderService");
}
/** @deprecated */
@Deprecated
public static ComponentName currentTimeProvider() {
return new ComponentName("com.google.android.wearable.app", "com.google.android.clockwork.home.complications.providers.CurrentTimeProvider");
}
/** @deprecated */
@Deprecated
public static ComponentName worldClockProvider() {
return new ComponentName("com.google.android.wearable.app", "com.google.android.clockwork.home.complications.providers.WorldClockProviderService");
}
/** @deprecated */
@Deprecated
public static ComponentName appsProvider() {
return new ComponentName("com.google.android.wearable.app", "com.google.android.clockwork.home.complications.providers.LauncherProviderService");
}
/** @deprecated */
@Deprecated
public static ComponentName stepCountProvider() {
return new ComponentName("com.google.android.wearable.app", "com.google.android.clockwork.home.complications.providers.StepsProviderService");
}
/** @deprecated */
@Deprecated
public static ComponentName unreadCountProvider() {
return new ComponentName("com.google.android.wearable.app", "com.google.android.clockwork.home.complications.providers.UnreadNotificationsProviderService");
}
/** @deprecated */
@Deprecated
public static ComponentName nextEventProvider() {
return new ComponentName("com.google.android.wearable.app", "com.google.android.clockwork.home.complications.providers.NextEventProviderService");
}
/** @deprecated */
@Deprecated
public static ComponentName retailStepCountProvider() {
return new ComponentName("com.google.android.wearable.app", "com.google.android.clockwork.home.complications.providers.RetailStepsProviderService");
}
/** @deprecated */
@Deprecated
public static ComponentName retailChatProvider() {
return new ComponentName("com.google.android.wearable.app", "com.google.android.clockwork.home.complications.providers.RetailChatProviderService");
}
/** @deprecated */
@Deprecated
public static ComponentName androidPayProvider() {
return new ComponentName("com.google.android.apps.walletnfcrel", "com.google.commerce.tapandpay.android.wearable.complications.PayProviderService");
}
@Retention(RetentionPolicy.SOURCE)
public @interface ProviderId {
}
}
二. 複雜數據是如何刷新的呢?
用戶自己實現複雜數據的方式,可參考https://developer.android.com/training/wearables/data-providers/exposing-data-complications。
複雜數據的刷新方式有三種:
1.主動刷新,也稱爲推送更新,是通過ProviderUpdateRequester來實現的。
ProviderUpdateRequester requester =
new ProviderUpdateRequester(context, new ComponentName(context, CurrentTimeProvider.class));
requester.requestUpdateAll();
2.在xml中指定默認刷新時間。
<meta-data
android:name="android.support.wearable.complications.UPDATE_PERIOD_SECONDS"
android:value="300" />
錶盤處於活動狀態時,會按照UPDATE_PERIOD_SECONDS
指定的頻率刷新複雜數據,如果複雜功能中顯示的信息不需要定期更新(例如當您使用推送更新時),請將該值設爲 0
。如果您未將 UPDATE_PERIOD_SECONDS
設爲 0
,則必須至少設爲 300
(5 分鐘),這是系統爲了節省設備電池電量而執行的最短更新週期。此外,請注意,當設備處於微光模式或未佩戴時,更新請求的頻率可能會降低。
3.一些依賴時效的字符串複雜數據,可以通過
ComplicationText.TimeFormatBuilder()來跟時間信息關聯起來,時間更新時home會自動更新這個數據的顯示,例如
String timePattern = (DateFormat.is24HourFormat(this)) ? "HH:mm" : "hh:mm";//12小時制也不顯示AM PM
if (type == ComplicationData.TYPE_SHORT_TEXT) {
data = new ComplicationData.Builder(ComplicationData.TYPE_SHORT_TEXT)
.setShortText(new ComplicationText.TimeFormatBuilder().setFormat(timePattern).
build())
.build();
} else {
Log.e(TAG, "onComplicationUpdate unsupport type " + type);
data = new ComplicationData.Builder(ComplicationData.TYPE_NO_DATA)
.build();
}
ComplicationText的build()會根據是否有時間format(例如"yyyy-mm-dd hh:mm:ss"/"HH:MM am"等時間格式化字符串)來判斷是否需要dependTime.
public ComplicationText build() {
if (this.mFormat == null) {
throw new IllegalStateException("Format must be specified.");
} else {
return new ComplicationText(this.mSurroundingText, new TimeFormatText(this.mFormat, this.mStyle, this.mTimeZone));
}
}
private ComplicationText(CharSequence surroundingText, TimeDependentText timeDependentText) {
this.mTemplateValues = new CharSequence[]{"", "^2", "^3", "^4", "^5", "^6", "^7", "^8", "^9"};
this.mSurroundingText = surroundingText;
this.mTimeDependentText = timeDependentText;
this.checkFields();
}
boolean isTimeDependent() {
return this.mTimeDependentText != null;
}
public static CharSequence getText(Context context, ComplicationText complicationText, long dateTimeMillis) {
return complicationText == null ? null : complicationText.getText(context, dateTimeMillis);
}
public CharSequence getText(Context context, long dateTimeMillis) {
if (this.mTimeDependentText == null) {
return this.mSurroundingText;
} else {
CharSequence timeDependentPart;
if (this.mDependentTextCache != null && this.mTimeDependentText.returnsSameText(this.mDependentTextCacheTime, dateTimeMillis)) {
timeDependentPart = this.mDependentTextCache;
} else {
timeDependentPart = this.mTimeDependentText.getText(context, dateTimeMillis);
this.mDependentTextCacheTime = dateTimeMillis;
this.mDependentTextCache = timeDependentPart;
}
if (this.mSurroundingText == null) {
return timeDependentPart;
} else {
this.mTemplateValues[0] = timeDependentPart;
return TextUtils.expandTemplate(this.mSurroundingText, this.mTemplateValues);
}
}
}
public interface TimeDependentText extends Parcelable {
CharSequence getText(Context context, long dateTimeMillis);
boolean returnsSameText(long firstDateTimeMillis, long secondDateTimeMillis);
long getNextChangeTime(long fromTime);
}
public long getNextChangeTime(long fromTime) {
return this.mTimeDependentText == null ? 9223372036854775807L : this.mTimeDependentText.getNextChangeTime(fromTime);
}
可以看出,ComplicationText獲取顯示內容時會傳遞一個時間戳,如果是
isTimeDependent返回true,也就是mTimeDependentText不爲null,就會調用
TimeDependentText的getText(Context context, long dateTimeMillis)來獲取顯示內容,
TimeDependentText是一個interface,繼續跟蹤實現它的地方,在android.support.wearable.complications.TimeFormatText中實現了接口。
public class TimeFormatText implements TimeDependentText {
private static final String[][] DATE_TIME_FORMAT_SYMBOLS = new String[][]{{"S", "s"}, {"m"}, {"H", "K", "h", "k", "j", "J", "C"}, {"a", "b", "B"}};
private static final long[] DATE_TIME_FORMAT_PRECISION;
private final SimpleDateFormat mDateFormat;
private final int mStyle;
private final TimeZone mTimeZone;
private final Date mDate;
private long mTimePrecision;
public static final Creator<TimeFormatText> CREATOR;
public TimeFormatText(String format, int style, TimeZone timeZone) {
this.mDateFormat = new SimpleDateFormat(format);
this.mStyle = style;
this.mTimePrecision = -1L;
if (timeZone != null) {
this.mDateFormat.setTimeZone(timeZone);
this.mTimeZone = timeZone;
} else {
this.mTimeZone = this.mDateFormat.getTimeZone();
}
this.mDate = new Date();
}
@SuppressLint({"DefaultLocale"})
public CharSequence getText(Context context, long dateTimeMillis) {
String formattedDate = this.mDateFormat.format(new Date(dateTimeMillis));
switch(this.mStyle) {
case 2:
return formattedDate.toUpperCase();
case 3:
return formattedDate.toLowerCase();
default:
return formattedDate;
}
}
public boolean returnsSameText(long firstDateTimeMillis, long secondDateTimeMillis) {
long precision = this.getPrecision();
firstDateTimeMillis += this.getOffset(firstDateTimeMillis);
secondDateTimeMillis += this.getOffset(secondDateTimeMillis);
return firstDateTimeMillis / precision == secondDateTimeMillis / precision;
}
public long getNextChangeTime(long fromTime) {
long precision = this.getPrecision();
long offset = this.getOffset(fromTime);
return ((fromTime + offset) / precision + 1L) * precision - offset;
}
//省略......
}
可以看出在getText時,還可以通過制定mStyle來選擇是否大小寫顯示。默認是1小寫。
三、遇到的問題。
開發階段遇到兩個問題,一個是設置時區後TimeDepend複雜數據未更新,另一個是在Offload模式下,小時分鐘顯示錯誤,一般是誤差5分鐘的整數倍。
a.時區設置後數據不更新
在開發中,我自己實現了一個TimeDenpend的複雜數據,但發現修改時區後錶盤信息並未刷新,但用系統提供的SystemProviders.TIME_AND_DATE是可以根據時區變化來刷新數據的。猜測Home會對自己提供的複雜數據特殊處理,例如時區變化時刷新時鐘相關的複雜數據,跟蹤WearOS Home一探究性。
以
CurrentTimeProvider爲例
lingx@ubuntu:~/work/vendor/google_clockwork_partners/packages/ClockworkHome$ grep -nr 'CurrentTimeProvider' *
AndroidManifest.xml:1202: android:name="com.google.android.clockwork.home.complications.providers.CurrentTimeProvider"
Binary file java/com/google/android/clockwork/home/module/complications/.ComplicationProvidersModule.java.swp matches
java/com/google/android/clockwork/home/module/complications/ComplicationProvidersModule.java:13:import com.google.android.clockwork.home.complications.providers.CurrentTimeProvider;
java/com/google/android/clockwork/home/module/complications/ComplicationProvidersModule.java:101: new ProviderUpdateRequester(context, new ComponentName(context, CurrentTimeProvider.class));
java/com/google/android/clockwork/home/complications/ProviderGetter.java:41: "com.google.android.clockwork.home.complications.providers.CurrentTimeProvider";
java/com/google/android/clockwork/home/complications/providers/CurrentTimeProvider.java:8:public class CurrentTimeProvider extends ComplicationProviderService {
java/com/google/android/clockwork/home/complications/providers/CurrentTimeDataBuilder.java:9:/** Creates complication data for use by the {@link CurrentTimeProvider}. */
java/com/google/android/clockwork/home/complications/SystemProviderMappingImpl.java:24: "com.google.android.clockwork.home.complications.providers.CurrentTimeProvider";
lingx@ubuntu:~/work/vendor/google_clockwork_partners/packages/ClockworkHome$
簡單瞭解下SystemProviderMappingImpl、ProviderGetter.java、ComplicationProvidersModule.java。
其中SystemProviderMappingImpl做了一個映射,供SystemProviders使用。
public class SystemProviderMappingImpl implements SystemProviderMapping {
private static final String HOME_PACKAGE_NAME = "com.google.android.wearable.app";
private static final String BATTERY_CLASS_NAME =
"com.google.android.clockwork.home.complications.providers.BatteryProviderService";
private static final String DATE_CLASS_NAME =
"com.google.android.clockwork.home.complications.providers.DayOfMonthProviderService";
private static final String DAY_AND_DATE_CLASS_NAME =
"com.google.android.clockwork.home.complications.providers.DateDayOfWeekProviderService";
private static final String DAY_OF_WEEK_CLASS_NAME =
"com.google.android.clockwork.home.complications.providers.DayOfWeekProviderService";
private static final String CURRENT_TIME_CLASS_NAME =
"com.google.android.clockwork.home.complications.providers.CurrentTimeProvider";
private static final String STEPS_CLASS_NAME =
"com.google.android.clockwork.home.complications.providers.StepsProviderService";
private static final String NEXT_EVENT_CLASS_NAME =
"com.google.android.clockwork.home.complications.providers.NextEventProviderService";
private static final String SUNRISE_SUNSET_CLASS_NAME =
"com.google.android.clockwork.home.complications.providers.SunriseSunsetProviderService";
private static final String WORLD_CLOCK_CLASS_NAME =
"com.google.android.clockwork.home.complications.providers.WorldClockProviderService";
private static final String APPS_CLASS_NAME =
"com.google.android.clockwork.home.complications.providers.LauncherProviderService";
private static final String MOST_RECENT_APP_CLASS_NAME =
"com.google.android.clockwork.home.complications.providers.MostRecentAppProviderService";
private static final String UNREAD_CLASS_NAME =
"com.google.android.clockwork.home.complications.providers"
+ ".UnreadNotificationsProviderService";
private static final String RETAIL_STEPS_CLASS_NAME =
"com.google.android.clockwork.home.complications.providers.RetailStepsProviderService";
private static final String RETAIL_CHAT_CLASS_NAME =
"com.google.android.clockwork.home.complications.providers.RetailChatProviderService";
private static final String PAY_PACKAGE_NAME = "com.google.android.apps.walletnfcrel";
private static final String PAY_CLASS_NAME =
"com.google.commerce.tapandpay.android.wearable.complications.PayProviderService";
private static final String FAVORITE_CONTACT_CLASS_NAME =
"com.google.android.clockwork.home.contacts.ContactsComplicationProviderService";
@Nullable
@Override
public ComponentName getSystemProviderComponent(@ProviderId int systemProvider) {
switch (systemProvider) {
case SystemProviders.WATCH_BATTERY:
return new ComponentName(HOME_PACKAGE_NAME, BATTERY_CLASS_NAME);
case SystemProviders.DATE:
return new ComponentName(HOME_PACKAGE_NAME, DATE_CLASS_NAME);
case SystemProviders.DAY_AND_DATE:
return new ComponentName(HOME_PACKAGE_NAME, DAY_AND_DATE_CLASS_NAME);
case SystemProviders.DAY_OF_WEEK:
return new ComponentName(HOME_PACKAGE_NAME, DAY_OF_WEEK_CLASS_NAME);
case SystemProviders.TIME_AND_DATE:
return new ComponentName(HOME_PACKAGE_NAME, CURRENT_TIME_CLASS_NAME);
case SystemProviders.STEP_COUNT:
return new ComponentName(HOME_PACKAGE_NAME, STEPS_CLASS_NAME);
case SystemProviders.WORLD_CLOCK:
return new ComponentName(HOME_PACKAGE_NAME, WORLD_CLOCK_CLASS_NAME);
case SystemProviders.APP_SHORTCUT:
return new ComponentName(HOME_PACKAGE_NAME, APPS_CLASS_NAME);
case SystemProviders.MOST_RECENT_APP:
return new ComponentName(HOME_PACKAGE_NAME, MOST_RECENT_APP_CLASS_NAME);
case SystemProviders.UNREAD_NOTIFICATION_COUNT:
return new ComponentName(HOME_PACKAGE_NAME, UNREAD_CLASS_NAME);
case SystemProviders.NEXT_EVENT:
return new ComponentName(HOME_PACKAGE_NAME, NEXT_EVENT_CLASS_NAME);
case SystemProviders.RETAIL_STEP_COUNT:
return new ComponentName(HOME_PACKAGE_NAME, RETAIL_STEPS_CLASS_NAME);
case SystemProviders.RETAIL_CHAT:
return new ComponentName(HOME_PACKAGE_NAME, RETAIL_CHAT_CLASS_NAME);
case SystemProviders.ANDROID_PAY:
return new ComponentName(PAY_PACKAGE_NAME, PAY_CLASS_NAME);
case SystemProviders.SUNRISE_SUNSET:
return new ComponentName(HOME_PACKAGE_NAME, SUNRISE_SUNSET_CLASS_NAME);
case SystemProviders.FAVORITE_CONTACT:
return new ComponentName(HOME_PACKAGE_NAME, FAVORITE_CONTACT_CLASS_NAME);
default:
return null;
}
}
}
ProviderGetter.java是給ProviderChooserController提供接口的,並會檢查複雜數據的包名,複雜數據配置時,會啓動
ComplicationHelperActivity,ComplicationHelperActivity會啓動ProviderChooserController來選擇複雜數據。
<provider
859 android:name="com.google.android.clockwork.home.application.LongLivedProcessProvider"
860 android:authorities="com.google.android.wearable.app.process.longlived"
861 android:exported="false"
862 android:initOrder="100" />
863 <activity
864 android:name="com.google.android.clockwork.home.complications.ProviderChooserActivity"
865 android:exported="true"
866 android:taskAffinity=""
867 android:theme="@style/ComplicationSettings" >
868 <intent-filter>
869 <action android:name="com.google.android.clockwork.home.complications.ACTION_CHOOSE_PROVIDER" />
870
871 <category android:name="android.intent.category.DEFAULT" />
872 </intent-filter>
873 </activity>
與複雜數據刷新相關的,重點要看的是ComplicationProvidersModule.java
package com.google.android.clockwork.home.module.complications;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.support.annotation.MainThread;
import android.support.wearable.complications.ProviderUpdateRequester;
import com.google.android.clockwork.common.io.IndentingPrintWriter;
import com.google.android.clockwork.home.complications.DefaultComplicationManager;
import com.google.android.clockwork.home.complications.providers.BatteryProviderService;
import com.google.android.clockwork.home.complications.providers.CurrentMediaProviderService;
import com.google.android.clockwork.home.complications.providers.CurrentTimeProvider;
import com.google.android.clockwork.home.complications.providers.DayAndDateProviderService;
import com.google.android.clockwork.home.complications.providers.DayOfMonthProviderService;
import com.google.android.clockwork.home.complications.providers.DayOfWeekProviderService;
import com.google.android.clockwork.home.complications.providers.LauncherProviderService;
import com.google.android.clockwork.home.complications.providers.MostRecentAppProviderService;
import com.google.android.clockwork.home.complications.providers.SunriseSunsetProviderService;
import com.google.android.clockwork.home.complications.providers.UnreadNotificationsProviderService;
import com.google.android.clockwork.home.events.BatteryChargeStateEvent;
import com.google.android.clockwork.home.events.MediaChangeEvent;
import com.google.android.clockwork.home.events.NotificationCountEvent;
import com.google.android.clockwork.home.events.PackageChangedEvent;
import com.google.android.clockwork.home.events.TimeZoneChangedEvent;
import com.google.android.clockwork.home.moduleframework.BasicModule;
import com.google.android.clockwork.home.moduleframework.ModuleBus;
import com.google.android.clockwork.home.moduleframework.eventbus.Subscribe;
import com.google.android.libraries.performance.primes.tracing.PrimesTrace;
@MainThread
public final class ComplicationProvidersModule implements BasicModule {
private final Context context;
private ModuleBus moduleBus;
private final DefaultComplicationManager complicationManager;
private int unreadStreamItemCount = -1;
public ComplicationProvidersModule(Context context) {
this.context = context;
complicationManager = DefaultComplicationManager.INSTANCE.get(context);
}
@Override
@PrimesTrace("ComplicationProvidersModule.initialize")
public void initialize(ModuleBus moduleBus) {
this.moduleBus = moduleBus;
this.moduleBus.register(this);
}
@Override
public void destroy() {}
@Subscribe
public void onBatteryChargeState(BatteryChargeStateEvent ev) {
ProviderUpdateRequester requester =
new ProviderUpdateRequester(
context, new ComponentName(context, BatteryProviderService.class));
requester.requestUpdateAll();
}
@Subscribe
public void onNotificationCount(NotificationCountEvent ev) {
if (ev.getUnreadCount() == unreadStreamItemCount) {
return;
}
unreadStreamItemCount = ev.getUnreadCount();
ProviderUpdateRequester requester =
new ProviderUpdateRequester(
context, new ComponentName(context, UnreadNotificationsProviderService.class));
requester.requestUpdateAll();
}
@Subscribe
public void onMediaChange(MediaChangeEvent ev) {
ProviderUpdateRequester requester =
new ProviderUpdateRequester(
context, new ComponentName(context, CurrentMediaProviderService.class));
requester.requestUpdateAll();
}
@Subscribe
public void onTimeZoneChanged(TimeZoneChangedEvent ev) {
ProviderUpdateRequester requester =
new ProviderUpdateRequester(
context, new ComponentName(context, DayOfMonthProviderService.class));
requester.requestUpdateAll();
requester =
new ProviderUpdateRequester(
context, new ComponentName(context, DayOfWeekProviderService.class));
requester.requestUpdateAll();
requester =
new ProviderUpdateRequester(
context, new ComponentName(context, DayAndDateProviderService.class));
requester.requestUpdateAll();
requester =
new ProviderUpdateRequester(context, new ComponentName(context, CurrentTimeProvider.class));
requester.requestUpdateAll();
requester =
new ProviderUpdateRequester(
context, new ComponentName(context, SunriseSunsetProviderService.class));
requester.requestUpdateAll();
}
@Subscribe
public void onPackageStatusChanged(PackageChangedEvent ev) {
// For LauncherProviderService.
if (Intent.ACTION_PACKAGE_REMOVED.equals(ev.getAction())) {
if (!ev.isReplacing()) {
complicationManager.removeConfigForPackage(ev.getPackageName());
}
// Requests an update for LauncherProviderService.
ProviderUpdateRequester requester =
new ProviderUpdateRequester(
context, new ComponentName(context, LauncherProviderService.class));
requester.requestUpdateAll();
}
// For MostRecentAppProviderService
if (!Intent.ACTION_PACKAGE_REMOVED.equals(ev.getAction()) || !ev.isReplacing()) {
ProviderUpdateRequester requester =
new ProviderUpdateRequester(
context, new ComponentName(context, MostRecentAppProviderService.class));
requester.requestUpdateAll();
}
}
@Override
public void dumpState(IndentingPrintWriter ipw, boolean verbose) {
complicationManager.dumpState(ipw, verbose);
}
}
可見,home監聽了時區、電量狀態、消息提醒、播放狀態信息,來主動刷新SystemProviders。
比較合理的做法應該是home檢測到時區變化,然後刷新複雜數據,理論上所有timedepend的複雜數據都應該刷新,無論是系統提供的還是用戶自定義的,看來目前時區變化後home只刷了系統複雜數據,沒有去刷別的複雜數據,這就需要用戶自己監聽時區變化再刷新複雜數據。
<receiver android:name=".complications.TimeZoneReceiver">
<intent-filter>
<action android:name="android.intent.action.TIMEZONE_CHANGED" />
</intent-filter>
</receiver>
public class TimeZoneReceiver extends BroadcastReceiver {
private static final String TAG = "TimeZoneReceiver";
private void freshAmbientComplications(Context context) {
ProviderUpdateRequester requester =
new ProviderUpdateRequester(
context, xxxx);
requester.requestUpdateAll();
}
@SuppressLint("UnsafeProtectedBroadcastReceiver")
@Override
public void onReceive(Context context, Intent intent) {
Log.i(TAG, "onReceive " + intent);
freshAmbientComplications(context);
}
}
b.Offload繪製小時分鐘複雜數據顯示異常。
offload模式有BG繪製,其他模式是由AP繪製的。在高通3100的MCU架構中,爲了省電,使用了多cpu交互模式。如下圖所示,BG和AP是兩個不同的cpu核兒。其中Application process跑WearOS系統,sensor算法主要跑在MDSP上,BlackGhost是BG的全稱,是一個主頻較低功耗較低的cpu。
可以看出AP繪製和BG繪製是在不同的cpu。
現在遇到的問題是BG繪製CurrentTimeProvider的數據時,顯示的"小時:分鐘",有概率出現比當前時間早5分鐘的整數倍的時間信息。AP測的繪製是沒有出現問題的。
先看下複雜數據provider的實現:
public class CurrentTimeProvider extends ComplicationProviderService {
@Override
public void onComplicationUpdate(int complicationId, int type, ComplicationManager manager) {
CurrentTimeDataBuilder dataBuilder =
new CurrentTimeDataBuilder(Locale.getDefault(), DateFormat.is24HourFormat(this));
manager.updateComplicationData(complicationId, dataBuilder.buildComplicationData(type));
}
}
final class CurrentTimeDataBuilder {
private static final String TAG = "CurrentTimeBuilder";
private final Locale locale;
private final boolean use24Hour;
CurrentTimeDataBuilder(Locale locale, boolean use24Hour) {
this.locale = locale;
this.use24Hour = use24Hour;
}
ComplicationData buildComplicationData(int type) {
ComplicationData data;
switch (type) {
case ComplicationData.TYPE_SHORT_TEXT:
data = buildCurrentTimeData();
break;
default:
data = new ComplicationData.Builder(ComplicationData.TYPE_NO_DATA).build();
if (Log.isLoggable(TAG, Log.WARN)) {
Log.w(TAG, "Unexpected complication type " + type);
}
}
return data;
}
private ComplicationData buildCurrentTimeData() {
ComplicationData data;
String timePattern = (DateFormat.is24HourFormat(this)) ? "HH:mm" : "hh:mm";//12小時制也不顯示AM PM
data =
new ComplicationData.Builder(ComplicationData.TYPE_SHORT_TEXT)
.setShortText(new ComplicationText.TimeFormatBuilder().setFormat(timePattern).build())
.build();
return data;
}
}
AP測的繪製邏輯在DecompositionDrawable中實現。
public class DecompositionDrawable extends Drawable {
......
public DecompositionDrawable(Context context) {
this.context = context;
}
public void draw(Canvas canvas) {
if (this.decomposition != null) {
Rect bounds = this.getBounds();
if (this.clipToCircle) {
canvas.save();
canvas.clipPath(this.roundPath);
}
this.converter.setPixelBounds(bounds);
Iterator var3 = this.drawnComponents.iterator();
while(true) {
DrawnComponent component;
do {
do {
if (!var3.hasNext()) {
if (this.inConfigMode) {
canvas.drawColor(this.context.getColor(color.config_scrim_color));
var3 = this.drawnComponents.iterator();
while(var3.hasNext()) {
component = (DrawnComponent)var3.next();
if (component instanceof ComplicationComponent) {
this.drawComplication((ComplicationComponent)component, canvas, this.converter);
}
}
}
if (this.clipToCircle) {
canvas.restore();
}
return;
}
component = (DrawnComponent)var3.next();
} while(this.inAmbientMode && !component.isAmbient());
} while(!this.inAmbientMode && !component.isInteractive());
if (component instanceof ImageComponent) {
this.drawImage((ImageComponent)component, canvas, this.converter);
} else if (component instanceof NumberComponent) {
this.drawNumber((NumberComponent)component, canvas, this.converter);
} else if (!this.inConfigMode && component instanceof ComplicationComponent) {
this.drawComplication((ComplicationComponent)component, canvas, this.converter);
}
}
}
}
private void drawComplication(ComplicationComponent component, Canvas canvas, CoordConverter converter) {
ComplicationDrawable drawable = (ComplicationDrawable)this.complicationDrawables.get(component.getWatchFaceComplicationId());
drawable.setCurrentTimeMillis(this.currentTimeMillis);
drawable.setInAmbientMode(this.inAmbientMode);
drawable.setBurnInProtection(this.burnInProtection);
drawable.setLowBitAmbient(this.lowBitAmbient);
RectF proportionalBounds = component.getBounds();
if (proportionalBounds != null) {
converter.getPixelRectFromProportional(proportionalBounds, this.boundsRect);
drawable.setBounds(this.boundsRect);
}
drawable.draw(canvas);
}
private void drawNumber(NumberComponent numberComponent, Canvas canvas, CoordConverter converter) {
if (!this.inAmbientMode || numberComponent.getMsPerIncrement() >= TimeUnit.MINUTES.toMillis(1L)) {
DigitDrawable digitDrawable = (DigitDrawable)this.fontDrawables.get(numberComponent.getFontComponentId());
if (digitDrawable != null) {
String digitString = numberComponent.getDisplayStringForTime(this.currentTimeMillis);
int maxDigits = (int)Math.log10((double)numberComponent.getHighestValue()) + 1;
PointF position = numberComponent.getPosition();
int digitWidth = digitDrawable.getIntrinsicWidth();
int digitHeight = digitDrawable.getIntrinsicHeight();
int x = converter.getPixelX(position.x);
x += digitWidth * (maxDigits - 1);
int y = converter.getPixelY(position.y);
this.boundsRect.set(x, y, x + digitWidth, y + digitHeight);
for(int i = digitString.length() - 1; i >= 0; --i) {
digitDrawable.setBounds(this.boundsRect);
digitDrawable.setCurrentDigit(Character.digit(digitString.charAt(i), 10));
digitDrawable.draw(canvas);
this.boundsRect.offset(-digitWidth, 0);
}
}
}
}
private void drawImage(ImageComponent imageComponent, Canvas canvas, CoordConverter converter) {
RotateDrawable drawable = (RotateDrawable)this.imageDrawables.get(imageComponent.getImage());
if (drawable != null) {
if (!this.inAmbientMode || imageComponent.getDegreesPerDay() < 518400.0F) {
converter.getPixelRectFromProportional(imageComponent.getBounds(), this.boundsRect);
drawable.setBounds(this.boundsRect);
float angle = this.angleForTime(imageComponent.getOffsetDegrees(), imageComponent.getDegreesPerDay());
angle = this.angleWithStep(angle, imageComponent.getDegreesPerStep());
drawable.setFromDegrees(angle);
drawable.setToDegrees(angle);
if (angle > 0.0F) {
drawable.setPivotX((float)(converter.getPixelX(imageComponent.getPivot().x) - this.boundsRect.left));
drawable.setPivotY((float)(converter.getPixelY(imageComponent.getPivot().y) - this.boundsRect.top));
}
drawable.setLevel(drawable.getLevel() + 1);
drawable.draw(canvas);
}
}
}
.........
}
由於Offload模式下顯示的特徵,是不能直接顯示字符串的,只能顯示有限的component,包括ComplicationComponent、FontComponent、NumberComponent、ImageComponent。要顯示字符串如"3月16 週一",就需要把字符串build成相應的component,跟蹤WearOs Home源碼發現,是將字符串轉成了FontComponent+NumberComponent,這些component都是drawable,然後通過SPI傳給BG(SidekickManager),由BG來負責顯示。目前看不到BG測的代碼,只能先分析AP端的,看下com.google.android.clockwork.home.watchfaces.OffloadControllerImpl.java和WatchFaceController、WatchFaceModule。
DecomposableWatchFace通過
updateDecomposition(@Nullable WatchFaceDecomposition decomposition)
會調用WatchFaceController的updateDecomposition來通知home。
/**
* Abstraction for controlling watch faces that are implemented as wallpapers. Hides from the
* Activity all interaction with the wallpaper.
*/
public class WatchFaceController implements Dumpable {
.......
@Override
public void updateDecomposition(@Nullable WatchFaceDecomposition decomposition) {
receivedDecomposition = true;
/*
* The config is set to indicate whether the current set watchface is decomposable. Other
* parts of the system such as Settings need to know whether the current watchface is
* decomposable or not, but such information is only known to Home. By setting this property,
* we pass this information to other system apps, see details in b/134506713.
*/
ambientConfig.setCurrentWatchfaceDecomposable(true);
if (decomposition == null) {
exitAmbientOffload();
offloadController.clearDecomposition();
} else {
offloadController.sendDecomposition(
decomposition,
success -> {
if (success && watchFaceVisibility.isInAmbient()) {
offloadController.enterAmbient(maybeStartOffloadRunnable);
}
});
}
}
}
.......
}
分析下offloadController.sendDecomposition(
decomposition,
success -> {
if (success && watchFaceVisibility.isInAmbient()) {
offloadController.enterAmbient(maybeStartOffloadRunnable);
}
});
sendDecomposition實現的地方在OffloadControllerImpl
/** Controls sending watch face decompositions to sidekick. */
public class OffloadControllerImpl implements OffloadController {
OffloadControllerImpl(
Context context,
SidekickManagerAsync sidekickManager,
ScreenConfiguration screenConfig,
BurnInConfig burnInConfig,
OffscreenRenderer offscreenRenderer,
Clock clock) {
this.context = context;
this.sidekickManager = checkNotNull(sidekickManager);//
this.burnInConfig = burnInConfig;//是否使用防燒屏。
this.offscreenRenderer = checkNotNull(offscreenRenderer);
this.clock = clock;
this.updateScheduler = new AmbientUpdateScheduler(context, clock);
coordConverter = new CoordConverter();
coordConverter.setPixelBounds(0, 0, screenConfig.getWidthPx(), screenConfig.getHeightPx());
}
@Override
public void sendDecomposition(
WatchFaceDecomposition decomposition, @Nullable SidekickManagerAsync.Callback callback) {
synchronized (lock) {
if (sendingTwmWatchFaceActivatesTwm() && receivedTwmDecomposition) {
return;
}
saveAmbientComplications(decomposition);
com.google.android.clockwork.decomposablewatchface.WatchFaceDecomposition decomposition2 =
convertAmbientDecomposition(decomposition, false);
if (decomposition2 != null) {
sidekickManager.sendWatchFace(decomposition2, false, callback);
sentDecomposition = true;
} else {
clearDecomposition();
}
}
}
@Override
public boolean enterAmbient(@Nullable Runnable onUpdatesSent) {
synchronized (lock) {
if (!sentDecomposition) {
return false;
}
inAmbientOffload = true;
updateScheduler.setInAmbientOffload(true);
long currentTime = clock.getCurrentTimeMs();
long nextMinute = nextMinuteBoundary(currentTime);
com.google.android.clockwork.decomposablewatchface.WatchFaceDecomposition.Builder builder =
new com.google.android.clockwork.decomposablewatchface.WatchFaceDecomposition.Builder();
boolean needReplace = false;
for (int i = 0; i < complications.size(); i++) {
Complication complication = complications.valueAt(i);
complication.updateTask.setNextUpdateTimeMs(nextMinute);
if (complication.needsDraw) {
addComplicationComponents(builder, complication, currentTime);
needReplace = true;
}
}
if (overlayRedraw) {
overlayUpdateTask.setNextUpdateTimeMs(nextMinute);
builder.addImageComponents(convertImageComponent(overlayComponent));
needReplace = true;
overlayRedraw = false;
}
if (needReplace) {
sidekickManager.replaceWatchFaceComponents(
builder.build(), onUpdatesSent == null ? null : success -> onUpdatesSent.run());
} else if (onUpdatesSent != null) {
onUpdatesSent.run();
}
}
return true;
}
private void saveAmbientComplications(WatchFaceDecomposition decomposition) {
synchronized (lock) {
complications.clear();
int currentComponentId = WatchFaceDecomposition.MAX_COMPONENT_ID + COMPONENT_ID_STEP;
for (ComplicationComponent component : decomposition.getComplicationComponents()) {
if (!component.isAmbient()) {
continue;
}
ComplicationDrawable drawable;
ComplicationDrawable providedDrawable = component.getComplicationDrawable();
if (providedDrawable == null) {
drawable = new ComplicationDrawable();
} else {
drawable = new ComplicationDrawable(providedDrawable);
}
// Set low bit ambient to disable anti-aliasing.
drawable.setContext(context);
drawable.setInAmbientMode(true);
drawable.setLowBitAmbient(true);
drawable.setBurnInProtection(burnInConfig.isProtectionEnabled());
coordConverter.getPixelRectFromProportional(component.getBounds(), workingRect);
workingRect.offset(-workingRect.left, -workingRect.top);
drawable.setBounds(workingRect);
Complication complication = new Complication(component, drawable, currentComponentId);
int wfComplicationId = component.getWatchFaceComplicationId();
complication.callback = new ComplicationCallback(wfComplicationId);
complication.drawable.setCallback(complication.callback);
ComplicationData data = complicationDatas.get(wfComplicationId);
if (data != null) {
complication.drawable.setComplicationData(data);
complication.needsDraw = true;
}
complication.updateTask =
updateScheduler.createTask(() -> doInvalidateComplication(complication));
//updateTask刷新錶盤的一個循環task。
complications.put(wfComplicationId, complication);
currentComponentId += COMPONENT_ID_STEP;
}
}
private void addComplicationComponents(
com.google.android.clockwork.decomposablewatchface.WatchFaceDecomposition.Builder builder,
Complication complication,
long currentTime) {
synchronized (lock) {
TimeDependentStrip strip = null;
int wfCompId = complication.component.getWatchFaceComplicationId();
ComplicationData data = complicationDatas.get(wfCompId);
if (data != null && data.isTimeDependent() && complicationStripSize >= 3) {
strip =
createRenderedComplicationFontStrip(complication, currentTime, complicationStripSize);
}
if (strip != null) {
builder.addFontComponents(strip.fontComponent);
builder.addNumberComponents(strip.numberComponent);
builder.addImageComponents(createBlankComponentForComplication(complication.component));
scheduleComplicationUpdateIfNeeded(complication, strip.lastUpdateTime);
} else {
strip = createDummyComplicationFontStrip(complication.component);
builder.addFontComponents(strip.fontComponent);
builder.addNumberComponents(strip.numberComponent);
builder.addImageComponents(
createRenderedComponentForComplication(complication, currentTime));
scheduleComplicationUpdateIfNeeded(complication, currentTime);
}
complication.needsDraw = false;
}
}
private TimeDependentStrip createRenderedComplicationFontStrip(
Complication complication, long currentTime, int size) {
synchronized (lock) {
ComplicationComponent component = complication.component;
ComplicationData data = complicationDatas.get(component.getWatchFaceComplicationId());
long[] frameTime = new long[size + 1];
int i;
// Fill in the update times and perform some basic sanity checks.
// frameTime[0] = current time
// frameTime[1] = first update
// ...
// frameTime[size - 1] = last frame
// frameTime[size] = scheduled invalidation
frameTime[0] = currentTime;
for (i = 1; i <= size; i++) {
frameTime[i] = determineNextChangeTime(data, frameTime[i - 1]);
if (frameTime[i] >= Long.MAX_VALUE
|| frameTime[i] <= frameTime[i - 1]
|| !data.isActive(frameTime[i - 1])) {
// We won't be able to properly schedule an update after the (i-1)'th frame. Stop there.
size = i - 1;
}
}
if (size < 3) {
// Not enough frames to determine update period
return null;
}
long updatePeriod = frameTime[2] - frameTime[1];
if (updatePeriod >= TimeUnit.DAYS.toMillis(1)) {
// Let's save some memory and use normal scheduling instead.
return null;
}
// Check the uniformity of updates
for (i = 1; i <= size; i++) {
long distance = frameTime[i] - frameTime[i - 1];
if (distance > updatePeriod || (distance < updatePeriod && i != 1 && i != size)) {
// It's fine only for the first update and the frame after the last one to come earlier.
size = i - 1;
}
}
if (size < 3) {
// Using a font strip with less than three frames is pointless
return null;
}
// Proceed to rendering the frames
RectF bounds = component.getBounds();
coordConverter.getPixelRectFromProportional(bounds, workingRect);
int width = workingRect.width();
int height = workingRect.height();
Bitmap partBitmap = Bitmap.createBitmap(width, height, Config.ARGB_8888);
Bitmap stripBitmap = Bitmap.createBitmap(width, height * size, Config.ARGB_8888);
Canvas partCanvas = new Canvas(partBitmap);
Canvas stripCanvas = new Canvas(stripBitmap);
stripCanvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
for (i = 0; i < size; i++) {
partCanvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
complication.drawable.draw(partCanvas, frameTime[i]);
stripCanvas.drawBitmap(partBitmap, 0, height * i, null);
}
partBitmap.recycle();
long startTime = frameTime[1] - updatePeriod;
startTime += TimeZone.getDefault().getOffset(startTime);
return new TimeDependentStrip(
new com.google.android.clockwork.decomposablewatchface.FontComponent.Builder()
.setComponentId(complication.componentId + 2)
.setImage(Icon.createWithBitmap(stripBitmap))
.setDigitCount(size)
.build(),
new com.google.android.clockwork.decomposablewatchface.NumberComponent.Builder()
.setComponentId(complication.componentId + 1)
.setZOrder(component.getZOrder())
.setFontComponentId(complication.componentId + 2)
.setMsPerIncrement(updatePeriod)
.setLowestValue(0)
.setHighestValue(size - 1)
.setPosition(new PointF(bounds.left, bounds.top))
.setTimeOffsetMs(-startTime)
.setMinDigitsShown(0)
.build(),
frameTime[size - 1]);
}
}
private void scheduleComplicationUpdateIfNeeded(Complication complication, long currentTime) {
synchronized (lock) {
int wfCompId = complication.component.getWatchFaceComplicationId();
ComplicationData data = complicationDatas.get(wfCompId);
long nextChangeTime = determineNextChangeTime(data, currentTime);
if (nextChangeTime < Long.MAX_VALUE) {
complication.updateTask.setNextUpdateTimeMs(nextChangeTime);
complication.updateTask.schedule();
}
}
}
}
}
對於time dependent的complication data,是一次獲取最近5次更新需要展示的內容,build成相應的component。
WatchFaceModule也監聽了,並且調用了invalidateTimeDependentComplications,會刷新顯示,但沒有刷新複雜數據,所以顯示的時間還是修改時區之前的。home裏有個ComplicationProvidersModule.java,這裏監聽了時區,時區變化時,是主動推送了系統的複雜數據刷新,因此我們自己實現的複雜數據要自己處理時區刷新。
OffloadControllerImpl,顯示覆雜數據的時候,是把數據轉成了FontComponent和NumberComponent,現在默認是5個長度,5個週期結束後,會通過complication.updateTask(doInvalidateComplication)重新刷新數據。FontComponent和NumberComponent的刷新,就跟之前純字體的錶盤一樣了,根據高度去摳圖顯示字符。
另外offload模式能不能按秒刷新。指針是可以按秒刷的,而且1秒能刷10幀,看上去就像平掃。但是複雜數據不能按秒刷。我看home的代碼 是以分鐘爲單位,測了一下,offload只能在整分鐘刷複雜數據,普通的ImageComponent是可以每秒刷的,還可以平掃,但複雜數據,按照現在的home只能整分時刷,因爲在AP判斷重新讀取複雜數據的時間轉成了分鐘的整數倍
private long determineNextChangeTime(ComplicationData data, long currentTime) {
if (data == null || !data.isTimeDependent()) {
return Long.MAX_VALUE;
}
long result = Long.MAX_VALUE;
result = getMinChangeTime(data.getShortText(), result, currentTime);
result = getMinChangeTime(data.getLongText(), result, currentTime);
result = getMinChangeTime(data.getShortTitle(), result, currentTime);
result = getMinChangeTime(data.getLongTitle(), result, currentTime);
return Math.max(result, nextMinuteBoundary(currentTime));
}
private long getMinChangeTime(ComplicationText text, long minTime, long currentTime) {
if (text == null) {
return minTime;
}
return Math.min(minTime, text.getNextChangeTime(currentTime));
}
private static long nextMinuteBoundary(long fromTime) {
return ((fromTime / MINUTES.toMillis(1)) + 1) * MINUTES.toMillis(1);
}
而對於NumberComponent、ImageComponent可以實現按秒刷新,最高頻率約每秒10幀。