神刀安全网

Something O’Clock – Part 5

On 1st April 2016 I published Something O’Clock , a watch face app for Android Wear, to Play Store. The app is lighthearted in nature (because of the date of publication), it allows the user to set the time to “beer o’clock”, or “sleep o’clock”, or even “burger o’clock”. Although the app itself is quite lighthearted the code behind it is worthy of study and, in this series we’ll take a look at various aspects of developing custom watch faces for Android Wear. In this article we’ll consider storing the current configuration within the wear app.

Something O’Clock – Part 5 Previously we looked at how the wear app receives the configuration changes made in the mobile companion app, and this component was effectively acting as the local proxy for the mobile companion app. The next thing that we need to consider is how to store the current configuration within the wear app. The mechanism that we’ll use for doing this is the same DataApi that we used to marshall the configuration changes – we’ll just use a different path within the URI that we use so that it is distinct from the remote data. For the remote transfer we use the URI wear://*/mobile , but for the local storage we’ll use wear://<actual host id>/wear instead. Let’s take a look at LocalDataMap which is responsible for this:

public class LocalDataMap {       public static final String PATH_WORDS = "/wear";       private final GoogleApiClientgoogleApiClient;       private final LocalUriFetcherlocalUriFetcher;     private final MissingDataPopulatormissingDataPopulator;       public static LocalDataMapnewInstance(Contextcontext, GoogleApiClientgoogleApiClient) {         LocalUriFetcherlocalUriFetcher = new LocalUriFetcher(googleApiClient);         MissingDataPopulatormissingDataPopulator = new MissingDataPopulator(context);         return new LocalDataMap(googleApiClient, localUriFetcher, missingDataPopulator);     }       public LocalDataMap(GoogleApiClientgoogleApiClient, LocalUriFetcherlocalUriFetcher, MissingDataPopulatormissingDataPopulator) {         this.googleApiClient = googleApiClient;         this.localUriFetcher = localUriFetcher;         this.missingDataPopulator = missingDataPopulator;     }       public static DataMapdataMapFrom(DataItemdataItem) {         DataMapItemdataMapItem = DataMapItem.fromDataItem(dataItem);         return dataMapItem.getDataMap();     }       public void overwriteConfig(final String word, final ResultCallback<DataApi.DataItemResult> callback) {         fetchConfig(new ResultCallback<DataMapResult>() {             @Override             public void onResult(@NonNullDataMapResultdataMapResult) {                 if (dataMapResult.getStatus().isSuccess()) {                     DataMapcurrentDataMap = dataMapResult.getDataMap();                     DataMapnewDataMap = new DataMap();                     newDataMap.putAll(currentDataMap);                     newDataMap.putString(CommonData.KEY_WORD, word);                     missingDataPopulator.populateMissingKeys(newDataMap);                     writeConfig(newDataMap, callback);                 }             }         });     }       public void fetchConfig(final ResultCallback<DataMapResult> callback) {         getLocalStorageUri(new ResultCallback<UriResult>() {             @Override             public void onResult(@NonNullUriResulturiResult) {                 Uriuri = uriResult.getUri();                 ResultCallback<DataApi.DataItemResult> populator = missingDataPopulator.getPopulator(LocalDataMap.this, callback);                 Wearable.DataApi.getDataItem(googleApiClient, uri).setResultCallback(populator);             }         });     }       public void getLocalStorageUri(ResultCallback<UriResult> callback) {         localUriFetcher.getLocalStorageUri(PATH_WORDS, callback);     }       public void writeConfig(DataMapdataMap, ResultCallback<DataApi.DataItemResult> callback) {         PutDataMapRequestputDataMapRequest = PutDataMapRequest.create(PATH_WORDS);         putDataMapRequest.setUrgent();         putDataMapRequest.getDataMap().putAll(dataMap);         Wearable.DataApi.putDataItem(googleApiClient, putDataMapRequest.asPutDataRequest())                 .setResultCallback(callback);     } } 

The main method that we call from our DataListenerService is overwriteConfig() which is responsible for applying changes. All of the calls are asynchronous so we just chain then up. First we need to fetch the existing data, then we write the new settings to this DataMap . We then make sure that we populate any missing values before finally writing the updated data.

Some ancillary classes are required by all of this. The LocalUriFetcher class is used to build the a URI for the local device with the local device id used in the host of the Uri to ensure that we only use local data:

class LocalUriFetcher {       private static final StatusSUCCESS = new Status(0);       private final GoogleApiClientgoogleApiClient;       public LocalUriFetcher(GoogleApiClientgoogleApiClient) {         this.googleApiClient = googleApiClient;     }       public void getLocalStorageUri(final String path, final ResultCallback<UriResult> callback) {         Wearable.NodeApi.getLocalNode(googleApiClient).setResultCallback(new ResultCallback<NodeApi.GetLocalNodeResult>() {             @Override             public void onResult(@NonNullNodeApi.GetLocalNodeResultgetLocalNodeResult) {                 String localNode = getLocalNodeResult.getNode().getId();                 Uriuri = new Uri.Builder()                         .scheme("wear")                         .path(path)                         .authority(localNode)                         .build();                 callback.onResult(new LocalStorageUriResult(uri));             }         });     }       private class LocalStorageUriResult implements UriResult {         private final Uriuri;           LocalStorageUriResult(Uriuri) {             this.uri = uri;         }           @Override         public UrigetUri() {             return uri;         }           @Override         public StatusgetStatus() {             return SUCCESS;         }     } } 

Next we have MissingDataPopulator which is responsible for filling in any missing data from a DataMap . There are two main use-cases where we need to do this: first before we save the data we want to fill in any missing fields, and this can be done using a synchronous call:

class MissingDataPopulator {       private final Contextcontext;       public MissingDataPopulator(Contextcontext) {         this.context = context;     }       public boolean populateMissingKeys(@NonNullDataMapdataMap) {         boolean shouldSave = false;         if (!dataMap.containsKey(CommonData.KEY_WORDS)) {             String[] wordsArray = context.getResources().getStringArray(R.array.default_words);             ArrayList<String> words = new ArrayList<>(Arrays.asList(wordsArray));             dataMap.putStringArrayList(CommonData.KEY_WORDS, words);             shouldSave = true;         }         if (!dataMap.containsKey(CommonData.KEY_WORD)) {             List<String> words = dataMap.getStringArrayList(CommonData.KEY_WORDS);             String defaultWord = words.get(0);             dataMap.putString(CommonData.KEY_WORD, defaultWord);             shouldSave = true;         }         return shouldSave;     }     .     .     . } 

If any data is missing then we populate it from the resources we created in the common module.

The second, slightly more complex use-case is when we load data we want to ensure that we add any missing data before passing it back to the caller. We do this in LocalDataMap by adding an additional step in the chain:

    public void fetchConfig(final ResultCallback<DataMapResult> callback) {         getLocalStorageUri(new ResultCallback<UriResult>() {             @Override             public void onResult(@NonNullUriResulturiResult) {                 Uriuri = uriResult.getUri();                 ResultCallback<DataApi.DataItemResult> populator = missingDataPopulator.getPopulator(LocalDataMap.this, callback);                 Wearable.DataApi.getDataItem(googleApiClient, uri).setResultCallback(populator);             }         });     } 

This retrieves the raw data, then passes it to populator (which populates any missing items), and when populator completes it will call the original callback. The populator implementation is also part of MissingDataPopulator :

class MissingDataPopulator {       private static final StatusSUCCESS = new Status(0);     .     .     .     public ResultCallback<DataApi.DataItemResult> getPopulator(LocalDataMaplocalDataMap, ResultCallback<DataMapResult> callback) {         return new Populator(localDataMap, callback);     }       private final class Populatorimplements ResultCallback<DataApi.DataItemResult> {         private final LocalDataMaplocalDataMap;         private final ResultCallback<DataMapResult> callback;           private Populator(LocalDataMaplocalDataMap, ResultCallback<DataMapResult> callback) {             this.localDataMap = localDataMap;             this.callback = callback;         }           @Override         public void onResult(@NonNullDataApi.DataItemResultdataItemResult) {             final DataMapdataMap;             if (hasConfig(dataItemResult)) {                 DataItemdataItem = dataItemResult.getDataItem();                 dataMap = LocalDataMap.dataMapFrom(dataItem);             } else {                 dataMap = new DataMap();             }             if (populateMissingKeys(dataMap)) {                 localDataMap.writeConfig(dataMap, new ResultCallback<DataApi.DataItemResult>() {                     @Override                     public void onResult(@NonNullDataApi.DataItemResultdataItemResult) {                         callback.onResult(new PopulatorResult(dataMap));                     }                 });                 return;             }             callback.onResult(new PopulatorResult(dataMap));         }           private boolean hasConfig(@NonNullDataApi.DataItemResultdataItemResult) {             return dataItemResult.getStatus().isSuccess() && dataItemResult.getDataItem() != null;         }           private final class PopulatorResult implements DataMapResult {             private final DataMapdataMap;               private PopulatorResult(DataMapdataMap) {                 this.dataMap = dataMap;             }               @Override             public DataMapgetDataMap() {                 return dataMap;             }               @Override             public StatusgetStatus() {                 return SUCCESS;             }           }     } } 

So if the actual data has been loaded then it will be used, otherwise we create an empty DataMap . Then we use the same populateMissingKeys() method we looked at previously to populate any missing data from the resources in the common module. If we have actually overridden anything then it will be saved before we make the callback with the loaded & populated DataMap . The nice thing that this gives us is that whenever we call fetchConfig() on LocalDataMap we know that we’ll receive valid data.

So now we are storing our data locally all that remains is to go back to where we started and hook it up the the watch-face UI, and we’ll look at this in the concluding article to this series.

Although we don’t yet have it hooked up to the UI, the source will actually compile and is available here .

Many thanks to Daniele Bonaldo, Sebastiano Poggi, Erik Hellman, Hasan Hosgel, Said Tahsin Dane & Murat Yener – my beta testers.

Something O’Clock – Part 5

© 2016,Mark Allison. All rights reserved.

转载本站任何文章请注明:转载至神刀安全网,谢谢神刀安全网 » Something O’Clock – Part 5

分享到:更多 ()

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址