1 /* 2 * Copyright 2022 Google LLC 3 * 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 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 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. 15 */ 16 package com.google.android.libraries.mobiledatadownload.internal.util; 17 18 import android.content.Context; 19 import android.content.SharedPreferences; 20 import android.util.Base64; 21 import com.google.common.base.Optional; 22 import com.google.errorprone.annotations.CheckReturnValue; 23 import com.google.protobuf.ExtensionRegistryLite; 24 import com.google.protobuf.InvalidProtocolBufferException; 25 import com.google.protobuf.MessageLite; 26 import com.google.protobuf.Parser; 27 import java.io.IOException; 28 import javax.annotation.Nullable; 29 30 /** 31 * Simple util to read/write protos from/to {@link SharedPreferences}. 32 * 33 * <p>Protos are serialized, and the binary value is base-64 encoded without padding or wrapping. 34 */ 35 @CheckReturnValue 36 public class SharedPreferencesUtil { 37 38 /** 39 * Reads the shared pref value corresponding to the specified key as a lite proto of type 'T'. The 40 * read value is populated to 'protoValue' which should already be constructed by the caller. 41 * 42 * @return the proto or null if no such element was found or could not be parsed. 43 */ 44 @Nullable readProto( SharedPreferences prefs, K liteKey, Parser<V> parser)45 public static <K extends MessageLite, V extends MessageLite> V readProto( 46 SharedPreferences prefs, K liteKey, Parser<V> parser) { 47 return readProto(prefs, serializeProto(liteKey), parser); 48 } 49 50 /** 51 * Reads the shared pref value corresponding to the specified key as a lite proto of type 'T'. The 52 * read value is populated to 'protoValue' which should already be constructed by the caller. 53 * 54 * @return the proto or null if no such element was found or could not be parse. 55 */ 56 @Nullable readProto( SharedPreferences prefs, String key, Parser<T> parser)57 public static <T extends MessageLite> T readProto( 58 SharedPreferences prefs, String key, Parser<T> parser) { 59 String encodedLiteString = prefs.getString(key, null); 60 if (encodedLiteString == null) { 61 return null; 62 } 63 try { 64 return parseLiteFromEncodedString(encodedLiteString, parser); 65 } catch (InvalidProtocolBufferException e) { 66 return null; 67 } 68 } 69 70 /** 71 * Write and commit the serialized form of the proto into pref, corresponding to the give proto 72 * key. 73 */ writeProto( SharedPreferences prefs, final String key, final T protoValue)74 public static <T extends MessageLite> boolean writeProto( 75 SharedPreferences prefs, final String key, final T protoValue) { 76 SharedPreferences.Editor editor = prefs.edit(); 77 writeProto(editor, key, protoValue); 78 return editor.commit(); 79 } 80 81 /** 82 * Write and commit the serialized form of the proto into pref, corresponding to the give proto 83 * key. 84 */ writeProto( SharedPreferences prefs, final K protoKey, final T protoValue)85 public static <K extends MessageLite, T extends MessageLite> boolean writeProto( 86 SharedPreferences prefs, final K protoKey, final T protoValue) { 87 SharedPreferences.Editor editor = prefs.edit(); 88 writeProto(editor, protoKey, protoValue); 89 return editor.commit(); 90 } 91 92 /** 93 * Write the serialized form of the proto into shared pref, corresponding to the given proto key. 94 */ writeProto( SharedPreferences.Editor editor, final K protoKey, final T protoValue)95 public static <K extends MessageLite, T extends MessageLite> void writeProto( 96 SharedPreferences.Editor editor, final K protoKey, final T protoValue) { 97 writeProto(editor, serializeProto(protoKey), protoValue); 98 } 99 100 /** 101 * Write the serialized form of the proto into shared pref, corresponding to the given string key. 102 */ writeProto( SharedPreferences.Editor editor, final String key, final T protoValue)103 public static <T extends MessageLite> void writeProto( 104 SharedPreferences.Editor editor, final String key, final T protoValue) { 105 editor.putString(key, serializeProto(protoValue)); 106 } 107 108 /** Removes whatever value corresponds the protoKey from shared prefs. */ removeProto( SharedPreferences prefs, String protoKey)109 public static <K extends MessageLite> boolean removeProto( 110 SharedPreferences prefs, String protoKey) { 111 return prefs.edit().remove(protoKey).commit(); 112 } 113 114 /** Removes whatever value corresponds the protoKey from shared prefs. */ removeProto( SharedPreferences.Editor editor, final K protoKey)115 public static <K extends MessageLite> void removeProto( 116 SharedPreferences.Editor editor, final K protoKey) { 117 editor.remove(serializeProto(protoKey)); 118 } 119 120 /** Removes whatever value corresponds the protoKey from shared prefs. */ removeProto(SharedPreferences.Editor editor, String protoKey)121 public static void removeProto(SharedPreferences.Editor editor, String protoKey) { 122 editor.remove(protoKey); 123 } 124 125 /** Converts a MessageLite to a string that can be used as a key in shared prefs. */ serializeProto(MessageLite lite)126 public static String serializeProto(MessageLite lite) { 127 byte[] byteValue = lite.toByteArray(); 128 return Base64.encodeToString(byteValue, Base64.NO_PADDING | Base64.NO_WRAP); 129 } 130 131 /** 132 * Parses a MessageLite from the base64 encoded string. 133 * 134 * @return the proto. 135 */ parseLiteFromEncodedString( String base64Encoded, Parser<T> parser)136 public static <T extends MessageLite> T parseLiteFromEncodedString( 137 String base64Encoded, Parser<T> parser) throws InvalidProtocolBufferException { 138 byte[] byteValue; 139 try { 140 byteValue = Base64.decode(base64Encoded, Base64.NO_PADDING | Base64.NO_WRAP); 141 } catch (IllegalArgumentException e) { 142 throw new InvalidProtocolBufferException( 143 "Unable to decode to byte array", new IOException(e)); 144 } 145 146 // Cannot use generated registry here, because it may cause NPE to clients. 147 // For more detail, see b/140135059. 148 return parser.parseFrom(byteValue, ExtensionRegistryLite.getEmptyRegistry()); 149 } 150 151 /** Returns the SharedPreferences name for {@code instanceId}. */ 152 // TODO(b/204094591): determine whether instanceId is ever actually null. getSharedPreferencesName(String baseName, Optional<String> instanceId)153 public static String getSharedPreferencesName(String baseName, Optional<String> instanceId) { 154 return instanceId != null && instanceId.isPresent() ? baseName + instanceId.get() : baseName; 155 } 156 157 /** Return the SharedPreferences for InstanceId */ 158 // TODO(b/204094591): determine whether instanceId is ever actually null. getSharedPreferences( Context context, String baseName, Optional<String> instanceId)159 public static SharedPreferences getSharedPreferences( 160 Context context, String baseName, Optional<String> instanceId) { 161 return context.getSharedPreferences( 162 getSharedPreferencesName(baseName, instanceId), Context.MODE_PRIVATE); 163 } 164 } 165