/* * Copyright 2010-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (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/apache2.0 * * or in the "license" file accompanying this file. This file is distributed * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either * express or implied. See the License for the specific language governing * permissions and limitations under the License. */ package com.amazonaws.mobileconnectors.util; import android.content.Context; import android.content.SharedPreferences; import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; import android.os.Build; import android.util.Log; import com.amazonaws.AmazonClientException; import com.amazonaws.util.Base64; import com.amazonaws.util.StringUtils; import org.json.JSONException; import org.json.JSONObject; import java.util.Locale; import java.util.Map; import java.util.UUID; /** * A class that consists of app info, device info, additional service info, and * user defined data. * * <pre> * <code>{@code * ClientContext clientContext = new ClientContext(activity); * * Map<String, String> map = new HashMap<String, String>(); * map.put("first_name", "John"); * map.put("last_name", "Doe"); * clientContext.putCustomContext(map); * * clientContext.putServiceContext("mobile_analytics", serviceContext); * * String base64 = clientContext.toBase64String(); * }</code> * </pre> */ public class ClientContext { private static final String TAG = "ClientContext"; /** * Name of the shared preferences where client id is saved. */ static final String SHARED_PREFERENCES = "com.amazonaws.common"; private final JSONObject json; private String base64String; /** * Constructs a new client context. * * @param context context of the app */ public ClientContext(Context context) { if (context == null) { throw new IllegalArgumentException("Context can't be null"); } json = new JSONObject(); try { json.put("client", getClientInfo(context)) .put("env", getDeviceInfo(context)); } catch (JSONException e) { throw new AmazonClientException("Failed to build client context", e); } } /** * Gets the installation id from shared preferences. A new one will be * assigned if not found. The installation id is unique per app * installation. This value appears as installation_id in client context * object in AWS Lambda. * * @param context context of the app * @return the unique installation id per app installation * @see <a * href="http://docs.aws.amazon.com/lambda/latest/dg/nodejs-prog-model-context.html">The * Context Object (Node.js)</a> */ public static String getInstallationId(Context context) { SharedPreferences sp = context.getSharedPreferences(SHARED_PREFERENCES, Context.MODE_PRIVATE); String installationId = sp.getString("installation_id", null); if (installationId == null) { installationId = UUID.randomUUID().toString(); sp.edit().putString("installation_id", installationId).commit(); } return installationId; } /** * Gets the client info, including installation_id_id, app_title, * app_version_name, app_version_code, and app_package_name. * * @param context context of the app * @return an JSONObject that has the client info * @throws JSONException */ static JSONObject getClientInfo(Context context) throws JSONException { JSONObject client = new JSONObject(); PackageManager packageManager = context.getPackageManager(); try { PackageInfo packageInfo = packageManager.getPackageInfo(context.getPackageName(), 0); ApplicationInfo applicationInfo = context.getApplicationInfo(); client.put("installation_id", getInstallationId(context)) .put("app_version_name", packageInfo.versionName) .put("app_version_code", String.valueOf(packageInfo.versionCode)) .put("app_package_name", packageInfo.packageName); // If null is returned for some reason, fall back to "Unknown" CharSequence title = packageManager.getApplicationLabel(applicationInfo); client.put("app_title", title == null ? "Unknown" : title.toString()); } catch (NameNotFoundException e) { // When device starts, PackageManager will gather package // information by scanning them. It will take a while to finish. // This exception may be thrown when scan doesn't finish. Log.w(TAG, "Failed to load package info: " + context.getPackageName(), e); } return client; } /** * Gets the device info, including platform, model, make, platform_version, * and locale. * * @param context context of the app * @return an JSONObject that has the device info * @throws JSONException */ static JSONObject getDeviceInfo(Context context) throws JSONException { JSONObject env = new JSONObject() .put("platform", "Android") .put("model", Build.MODEL) .put("make", Build.MANUFACTURER) .put("platform_version", Build.VERSION.RELEASE) .put("locale", Locale.getDefault().toString()); return env; } /** * Adds additional user defined key-value pairs to the client context under * "custom". This method is not thread safe. * * <pre> * <code>{@code * Map<String, String> map = new HashMap<String, String>(); * map.put("first_name", "John"); * map.put("last_name", "Doe"); * clientContext.putCustomContext(map); * }</code> * </pre> * * The above code will add a Json object under "custom" as following. * * <pre> * { * "custom": { * "first_name":"John", * "last_name":"Doe" * } * } * </pre> * * @param map the custom key-value context map */ public void putCustomContext(Map<String, String> map) { // clear serialized string when content is changed. base64String = null; try { json.put("custom", new JSONObject(map)); } catch (JSONException e) { throw new AmazonClientException("Failed to add user defined context", e); } } /** * Sets service context under key "services". This will overwrite the * service context of the same service name. This method isn't thread safe. * * <pre> * { * "services": { * "mobile_analytics": { * "app_id":"mobile_analytics_app_id" * } * } * } * </pre> * * @param service service name * @param map the service key-value context map */ public void putServiceContext(String service, Map<String, String> map) { // clear serialized string when content is changed. base64String = null; try { if (!json.has("services")) { json.put("services", new JSONObject()); } JSONObject services = json.getJSONObject("services"); services.put(service, new JSONObject(map)); } catch (JSONException e) { throw new AmazonClientException("Failed to add service context", e); } } /** * Serializes the client context into a base64 encoded Json string. * * @return a based64 encoded Json string */ public String toBase64String() { if (base64String == null) { synchronized (this) { if (base64String == null) { base64String = Base64 .encodeAsString(json.toString().getBytes(StringUtils.UTF8)); } } } return base64String; } /** * Gets the underlying JSONObject of this client context. Warning: any * change to the JSONObject will also change the client context. * * @return JSONObject that represents the client context */ JSONObject getJson() { return json; } }