WearOS複雜數據的刷新

錶盤可以通過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幀。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章