/** * Copyright 2013-2016 Amazon.com, * Inc. or its affiliates. All Rights Reserved. * * Licensed under the Amazon Software License (the "License"). * You may not use this file except in compliance with the * License. A copy of the License is located at * * http://aws.amazon.com/asl/ * * or in the "license" file accompanying this file. This file is * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR * CONDITIONS OF ANY KIND, express or implied. See the License * for the specific language governing permissions and * limitations under the License. */ package com.amazonaws.mobileconnectors.cognito; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.util.Log; import com.amazonaws.AmazonClientException; import com.amazonaws.ClientConfiguration; import com.amazonaws.auth.CognitoCachingCredentialsProvider; import com.amazonaws.auth.IdentityChangedListener; import com.amazonaws.mobileconnectors.cognito.exceptions.DataStorageException; import com.amazonaws.mobileconnectors.cognito.exceptions.RegistrationFailedException; import com.amazonaws.mobileconnectors.cognito.exceptions.UnsubscribeFailedException; import com.amazonaws.mobileconnectors.cognito.internal.storage.CognitoSyncStorage; import com.amazonaws.mobileconnectors.cognito.internal.storage.SQLiteLocalStorage; import com.amazonaws.mobileconnectors.cognito.internal.util.DatasetUtils; import com.amazonaws.regions.Region; import com.amazonaws.regions.Regions; import com.amazonaws.services.cognitosync.AmazonCognitoSyncClient; import com.amazonaws.services.cognitosync.model.RegisterDeviceRequest; import com.amazonaws.services.cognitosync.model.RegisterDeviceResult; import com.amazonaws.services.cognitosync.model.ResourceNotFoundException; import com.amazonaws.util.VersionInfoUtils; import java.util.ArrayList; import java.util.List; /** * This saves {@link Dataset} in SQLite database. Here is a sample usage: * * <pre> * CognitoCachingCredentialsProvider provider = new CognitoCachingCredentialsProvider(context, * awsAccountId, identityPoolId, unauthRoleArn, authRoleArn, Regions.US_EAST_1); * CognitoClientManager client = new CognitoClientManager(context, Regions.US_EAST_1, provider); * * Dataset dataset = client.openOrCreateDataset("default_dataset"); * dataset.put("high_score", "100"); * dataset.synchronize(new SyncCallback() { * // override callbacks * }); * </pre> */ public class CognitoSyncManager { private static final String TAG = "CognitoSyncManager"; /** * User agent string to append to all requests to the remote service */ private static final String USER_AGENT = CognitoSyncManager.class.getName() + "/" + VersionInfoUtils.getVersion(); /** * Default database name. */ private static final String DATABASE_NAME = "cognito_dataset_cache.db"; /** * The local storage is singleton to avoid SQLite resource leak and thread * contention. */ private static SQLiteLocalStorage local; private final Context context; private final CognitoSyncStorage remote; private final CognitoCachingCredentialsProvider provider; private final AmazonCognitoSyncClient syncClient; private final String identityPoolId; /** * Constructs a CognitoSyncManager object. * * @deprecated Please use the constructor without an identityPoolId * @param context a context of the app * @param identityPoolId Cognito identity pool id * @param region Cognito sync region * @param provider a credentials provider */ @Deprecated public CognitoSyncManager(Context context, String identityPoolId, Regions region, CognitoCachingCredentialsProvider provider) { this(context, region, provider); } /** * Final constructor. Package private to allow dependency injection * * @param context * @param region * @param provider * @param syncClient */ CognitoSyncManager(Context context, Regions region, CognitoCachingCredentialsProvider provider, AmazonCognitoSyncClient syncClient) { if (context == null) { throw new IllegalArgumentException("context can't be null"); } this.context = context; this.provider = provider; this.identityPoolId = provider.getIdentityPoolId(); synchronized (CognitoSyncManager.class) { if (local == null) { local = new SQLiteLocalStorage(context, DATABASE_NAME); } } this.syncClient = syncClient; syncClient.setRegion(Region.getRegion(region)); remote = new CognitoSyncStorage(identityPoolId, syncClient, provider); remote.setUserAgent(USER_AGENT); provider.registerIdentityChangedListener(new IdentityChangedListener() { @Override public void identityChanged(String oldIdentityId, String newIdentityId) { if (newIdentityId != null) { Log.i(TAG, "identity change detected"); local.changeIdentityId( oldIdentityId == null ? DatasetUtils.UNKNOWN_IDENTITY_ID : oldIdentityId, newIdentityId); } } }); } /** * Constructs a CognitoSyncManager object. * * @param context a context of the app * @param region Cognito sync region * @param provider a credentials provider */ public CognitoSyncManager(Context context, Regions region, CognitoCachingCredentialsProvider provider) { this(context, region, provider, new ClientConfiguration()); } /** * Constructs a CognitoSyncManager object. * * @param context a context of the app * @param region Cognito sync region * @param provider a credentials provider * @param clientConfiguration client configuration for underlying AWS client */ public CognitoSyncManager(Context context, Regions region, CognitoCachingCredentialsProvider provider, ClientConfiguration clientConfiguration) { this(context, region, provider, new AmazonCognitoSyncClient(provider, clientConfiguration)); } /** * Opens or creates a dataset. If the dataset doesn't exist, an empty one * with the given name will be created. Otherwise, dataset is loaded from * local storage. If a dataset is marked as deleted but hasn't been deleted * on remote via {@link #refreshDatasetMetadata()}, it will throw * {@link IllegalStateException}. * * @param datasetName dataset name, must be [a-zA-Z0=9_.:-]+ * @return dataset loaded from local storage */ public Dataset openOrCreateDataset(String datasetName) { DatasetUtils.validateDatasetName(datasetName); local.createDataset(getIdentityId(), datasetName); Dataset dataset = new DefaultDataset(context, datasetName, provider, local, remote); return dataset; } /** * Retrieves a list of datasets from local storage. It may not reflects * latest dataset on the remote storage until refreshDatasetMetadata is * called. * * @return list of datasets */ public List<DatasetMetadata> listDatasets() { return local.getDatasets(getIdentityId()); } /** * Refreshes dataset metadata. Dataset metadata is pulled from remote * storage and stored in local storage. Their record data isn't pulled down * until you sync each dataset. Note: this is a network request, so calling * this method in the main thread will result in * NetworkOnMainThreadException. * * @throws DataStorageException thrown when fail to refresh dataset metadata */ public void refreshDatasetMetadata() throws DataStorageException { List<DatasetMetadata> datasets = remote.getDatasets(); local.updateDatasetMetadata(getIdentityId(), datasets); } /** * Wipes all user data cached locally, including identity id, session * credentials, dataset metadata, and all records. Any data that hasn't been * synced will be lost. This method should be called after logging out a * customer. */ public void wipeData() { provider.clear(); local.wipeData(); Log.i(TAG, "All data has been wiped"); } String getIdentityId() { return DatasetUtils.getIdentityId(provider); } /** * Register device for push sync for the specified platform. Once this * device is registered and you have subscribed to a dataset, this device * will receive a push notification if the dataset subscribed to is changed * by any other device. * * @param platform Platform of the device, GCM or ADM * @param token Device token of the device, gotten when registered for the * platform in question. */ public void registerDevice(String platform, String token) { SharedPreferences sp = getSharedPreferences(); if (isDeviceRegistered()) { Log.i(TAG, "Device is already registered"); return; } String identityId = provider.getIdentityId(); RegisterDeviceRequest request = new RegisterDeviceRequest() .withIdentityPoolId(provider.getIdentityPoolId()) .withIdentityId(identityId) .withPlatform(platform) .withToken(token); try { RegisterDeviceResult result = syncClient.registerDevice(request); // Save this first as the namespacing below will need to look it up sp.edit().putString(namespaceId("platform"), platform).apply(); String deviceId = result.getDeviceId(); sp.edit().putString(namespaceIdPlatform("deviceId"), deviceId) .apply(); Log.i(TAG, "Device is registered successfully: " + deviceId); } catch (AmazonClientException ace) { Log.e(TAG, "Failed to register device", ace); throw new RegistrationFailedException("Failed to register device", ace); } } /** * Gets the push sync device id of the device in use. This device id * is unique per identity id, and helps identify the endpoint when Cognito is sending * push updates. * * @return the device id of the current user's device */ public String getDeviceId() { return getSharedPreferences().getString(namespaceIdPlatform("deviceId"), ""); } /** * Checks the cache to see if the registration information from a * registration with push synchronization is saved to the device for the * current identity. Device registrations are unique on a per * identity/platform basis. * * @return true if it has, false if it hasn't */ public boolean isDeviceRegistered() { if (provider.getCachedIdentityId() == null) { return false; } SharedPreferences sp = getSharedPreferences(); return !sp.getString(namespaceIdPlatform("deviceId"), "").isEmpty() && !sp.getString(namespaceId("platform"), "").isEmpty(); } /** * Unregisters the device for push sync for the current identity. This will * clear the local cache that blocks the device from registering, but not * clearing the information from outside the device. */ public void unregisterDevice() { if (provider.getCachedIdentityId() != null) { SharedPreferences sp = getSharedPreferences(); sp.edit().remove(namespaceIdPlatform("deviceId")) .remove(namespaceId("platform")) .apply(); } } /** * Subscribes the user to all datasets that the local device knows of for * push sync notifications, so that any changes to any of these datasets * will result in notifications to this device. */ public void subscribeAll() { List<String> datasetNames = new ArrayList<String>(); for (DatasetMetadata dataset : this.listDatasets()) { datasetNames.add(dataset.getDatasetName()); } subscribe(datasetNames); } /** * Subscribes the user to some set of datasets from the total list that the * device knows of, giving the user push sync notifications for all in that * set * * @param datasetNames The list of names of datasets to subscribe to */ public void subscribe(List<String> datasetNames) { for (String datasetName : datasetNames) { Dataset dataset = openOrCreateDataset(datasetName); dataset.subscribe(); } } /** * Unsubscribes the user to all datasets that the local device knows of from * push sync notifications, so that no changes to any of these datasets will * result in notifications to this device. */ public void unsubscribeAll() { List<String> datasetNames = new ArrayList<String>(); for (DatasetMetadata dataset : this.listDatasets()) { datasetNames.add(dataset.getDatasetName()); } unsubscribe(datasetNames); } /** * Unsubscribes the user to some set of datasets from the total list that * the device knows of, ending any reception of push sync notifications * * @param datasetNames The list of names of datasets to unsubscribe from */ public void unsubscribe(List<String> datasetNames) { for (String datasetName : datasetNames) { Dataset dataset = openOrCreateDataset(datasetName); try { dataset.unsubscribe(); } catch (UnsubscribeFailedException ufe) { if (ufe.getCause() instanceof ResourceNotFoundException) { Log.w(TAG, "Unable to unsubscribe to dataset " + datasetName + ", dataset not a subscription"); } else { throw ufe; } } } } /** * Converts the notification from Cognito push sync to an object that has * easy access to all of the relevant information. This should be called * only using a Cognito push synchronization message, anything else will * return an empty object. PushSyncUpdate.isPushSyncUpdate(Intent intent) * can be used to confirm. * * @param extras the bundle returned from the intent.getExtras() call * @return the PushSyncUpdate that bundle is converted to */ public PushSyncUpdate getPushSyncUpdate(Intent intent) { return new PushSyncUpdate(intent); } private SharedPreferences getSharedPreferences() { return context.getSharedPreferences("com.amazonaws.mobileconnectors.cognito", Context.MODE_PRIVATE); } // prefix the key with identity id and platform String namespaceIdPlatform(String key) { String platform = getSharedPreferences().getString(namespaceId("platform"), ""); return namespaceId(platform) + "." + key; } // prefix the name with the cached identity id String namespaceId(String key) { // This is only called if we've determined the cache isn't empty or // after a get id call. // It could also be called from a place on the main thread. As a result, // we check the cache // to do the namespacing by id. return provider.getCachedIdentityId() + "." + key; } /** * A helper method to close the underlying SQL storage. */ void close() { local.close(); } }