2 * Copyright 2011 Google Inc.
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
17 package com.google.ipc.invalidation.ticl.android2;
19 import com.google.ipc.invalidation.common.ObjectIdDigestUtils.Sha1DigestFunction;
20 import com.google.ipc.invalidation.external.client.SystemResources;
21 import com.google.ipc.invalidation.external.client.SystemResources.Logger;
22 import com.google.ipc.invalidation.ticl.proto.AndroidService.AndroidTiclState;
23 import com.google.ipc.invalidation.ticl.proto.AndroidService.AndroidTiclState.Metadata;
24 import com.google.ipc.invalidation.ticl.proto.AndroidService.AndroidTiclStateWithDigest;
25 import com.google.ipc.invalidation.ticl.proto.ClientProtocol.ApplicationClientIdP;
26 import com.google.ipc.invalidation.ticl.proto.ClientProtocol.ClientConfigP;
27 import com.google.ipc.invalidation.util.Bytes;
28 import com.google.ipc.invalidation.util.Preconditions;
29 import com.google.ipc.invalidation.util.ProtoWrapper.ValidationException;
30 import com.google.ipc.invalidation.util.TypedUtil;
32 import android.content.Context;
34 import java.io.DataInput;
35 import java.io.DataInputStream;
36 import java.io.FileInputStream;
37 import java.io.FileNotFoundException;
38 import java.io.FileOutputStream;
39 import java.io.IOException;
40 import java.util.Random;
44 * Class to save and restore instances of {@code InvalidationClient} to and from stable storage.
47 class TiclStateManager {
48 /** Name of the file to which Ticl state will be persisted. */
49 private static final String TICL_STATE_FILENAME = "android_ticl_service_state.bin";
52 * Maximum size of a Ticl state file. Files with larger size will be ignored as invalid. We use
53 * this because we allocate an array of bytes of the same size as the Ticl file and want to
54 * avoid accidentally allocating huge arrays.
56 private static final int MAX_TICL_FILE_SIZE_BYTES = 100 * 1024; // 100 kilobytes
58 /** Random number generator for created Ticls. */
59 private static final Random random = new Random();
62 * Restores the Ticl from persistent storage if it exists. Otherwise, returns {@code null}.
63 * @param context Android system context
64 * @param resources resources to use for the Ticl
66 static AndroidInvalidationClientImpl restoreTicl(Context context,
67 SystemResources resources) {
68 AndroidTiclState state = readTiclState(context, resources.getLogger());
72 AndroidInvalidationClientImpl ticl = new AndroidInvalidationClientImpl(context, resources,
74 setSchedulerId(resources, ticl);
78 /** Creates a new Ticl. Persistent stroage must not exist. */
79 static void createTicl(Context context, SystemResources resources, int clientType,
80 byte[] clientName, ClientConfigP config, boolean skipStartForTest) {
81 Preconditions.checkState(!doesStateFileExist(context), "Ticl already exists");
82 AndroidInvalidationClientImpl ticl = new AndroidInvalidationClientImpl(context, resources,
83 random, clientType, clientName, config);
84 if (!skipStartForTest) {
85 // Ticls are started when created unless this should be skipped for tests; we allow tests
86 // to skip starting Ticls because many integration tests assume that Ticls will not be
87 // started when created.
88 setSchedulerId(resources, ticl);
91 saveTicl(context, resources.getLogger(), ticl);
95 * Sets the scheduling id on the scheduler in {@code resources} to {@code ticl.getSchedulingId()}.
97 private static void setSchedulerId(SystemResources resources,
98 AndroidInvalidationClientImpl ticl) {
99 AndroidInternalScheduler scheduler =
100 (AndroidInternalScheduler) resources.getInternalScheduler();
101 scheduler.setTiclId(ticl.getSchedulingId());
105 * Saves a Ticl instance to persistent storage.
107 * @param context Android system context
108 * @param logger logger
109 * @param ticl the Ticl instance to save
111 static void saveTicl(Context context, Logger logger, AndroidInvalidationClientImpl ticl) {
112 FileOutputStream outputStream = null;
115 // Create a protobuf with the Ticl state and a digest over it.
116 AndroidTiclStateWithDigest digestedState = createDigestedState(ticl);
118 // Write the protobuf to storage.
119 outputStream = openStateFileForWriting(context);
120 outputStream.write(digestedState.toByteArray());
121 outputStream.close();
122 } catch (FileNotFoundException exception) {
123 logger.warning("Could not write Ticl state: %s", exception);
124 } catch (IOException exception) {
125 logger.warning("Could not write Ticl state: %s", exception);
128 if (outputStream != null) {
129 outputStream.close();
131 } catch (IOException exception) {
132 logger.warning("Exception closing Ticl state file: %s", exception);
138 * Reads and returns the Android Ticl state from persistent storage. If the state was missing
139 * or invalid, returns {@code null}.
141 static AndroidTiclState readTiclState(Context context, Logger logger) {
142 FileInputStream inputStream = null;
144 inputStream = openStateFileForReading(context);
145 DataInput input = new DataInputStream(inputStream);
146 long fileSizeBytes = inputStream.getChannel().size();
147 if (fileSizeBytes > MAX_TICL_FILE_SIZE_BYTES) {
148 logger.warning("Ignoring too-large Ticl state file with size %s > %s",
149 fileSizeBytes, MAX_TICL_FILE_SIZE_BYTES);
151 // Cast to int must be safe due to the above size check.
152 byte[] fileData = new byte[(int) fileSizeBytes];
153 input.readFully(fileData);
154 AndroidTiclStateWithDigest androidState = AndroidTiclStateWithDigest.parseFrom(fileData);
156 // Validate the digest in the method.
157 if (isDigestValid(androidState, logger)) {
158 return Preconditions.checkNotNull(androidState.getState(),
159 "validator ensures that state is set");
161 logger.warning("Android Ticl state failed digest check: %s", androidState);
164 } catch (FileNotFoundException exception) {
165 logger.info("Ticl state file does not exist: %s", TICL_STATE_FILENAME);
166 } catch (ValidationException exception) {
167 logger.warning("Could not read Ticl state: %s", exception);
168 } catch (IOException exception) {
169 logger.warning("Could not read Ticl state: %s", exception);
172 if (inputStream != null) {
175 } catch (IOException exception) {
176 logger.warning("Exception closing Ticl state file: %s", exception);
183 * Returns a {@link AndroidTiclStateWithDigest} containing {@code ticlState} and its computed
186 private static AndroidTiclStateWithDigest createDigestedState(
187 AndroidInvalidationClientImpl ticl) {
188 Sha1DigestFunction digester = new Sha1DigestFunction();
189 ApplicationClientIdP ticlAppId = ticl.getApplicationClientIdP();
190 Metadata metadata = Metadata.create(ticlAppId.getClientType(), ticlAppId.getClientName(),
191 ticl.getSchedulingId(), ticl.getConfig());
192 AndroidTiclState state = AndroidTiclState.create(ProtocolIntents.ANDROID_PROTOCOL_VERSION_VALUE,
193 ticl.marshal(), metadata);
194 digester.update(state.toByteArray());
195 AndroidTiclStateWithDigest verifiedState =
196 AndroidTiclStateWithDigest.create(state, new Bytes(digester.getDigest()));
197 return verifiedState;
200 /** Returns whether the digest in {@code state} is correct. */
201 private static boolean isDigestValid(AndroidTiclStateWithDigest state, Logger logger) {
202 Sha1DigestFunction digester = new Sha1DigestFunction();
203 digester.update(state.getState().toByteArray());
204 byte[] computedDigest = digester.getDigest();
205 if (!TypedUtil.<Bytes>equals(new Bytes(computedDigest), state.getDigest())) {
206 logger.warning("Android TICL state digest mismatch; computed %s for %s",
207 computedDigest, state);
213 /** Opens {@link #TICL_STATE_FILENAME} for writing. */
214 private static FileOutputStream openStateFileForWriting(Context context)
215 throws FileNotFoundException {
216 return context.openFileOutput(TICL_STATE_FILENAME, Context.MODE_PRIVATE);
219 /** Opens {@link #TICL_STATE_FILENAME} for reading. */
220 private static FileInputStream openStateFileForReading(Context context)
221 throws FileNotFoundException {
222 return context.openFileInput(TICL_STATE_FILENAME);
225 /** Deletes {@link #TICL_STATE_FILENAME}. */
226 public static void deleteStateFile(Context context) {
227 context.deleteFile(TICL_STATE_FILENAME);
230 /** Returns whether the state file exists, for tests. */
231 static boolean doesStateFileExistForTest(Context context) {
232 return doesStateFileExist(context);
235 /** Returns whether the state file exists. */
236 private static boolean doesStateFileExist(Context context) {
237 return context.getFileStreamPath(TICL_STATE_FILENAME).exists();
240 private TiclStateManager() {
241 // Disallow instantiation.