1 /* 2 * Copyright (C) 2019 The Android Open Source Project 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 android.view.textclassifier; 17 18 import android.annotation.Nullable; 19 import android.content.ContentResolver; 20 import android.content.Context; 21 import android.database.ContentObserver; 22 import android.provider.Settings; 23 import android.text.TextUtils; 24 import android.util.Base64; 25 import android.util.KeyValueListParser; 26 27 import com.android.internal.annotations.GuardedBy; 28 import com.android.internal.annotations.VisibleForTesting; 29 import com.android.internal.util.Preconditions; 30 31 import java.lang.ref.WeakReference; 32 import java.util.Objects; 33 import java.util.function.Supplier; 34 35 /** 36 * Parses the {@link Settings.Global#TEXT_CLASSIFIER_ACTION_MODEL_PARAMS} flag. 37 * 38 * @hide 39 */ 40 public final class ActionsModelParamsSupplier implements 41 Supplier<ActionsModelParamsSupplier.ActionsModelParams> { 42 private static final String TAG = TextClassifier.DEFAULT_LOG_TAG; 43 44 @VisibleForTesting 45 static final String KEY_REQUIRED_MODEL_VERSION = "required_model_version"; 46 @VisibleForTesting 47 static final String KEY_REQUIRED_LOCALES = "required_locales"; 48 @VisibleForTesting 49 static final String KEY_SERIALIZED_PRECONDITIONS = "serialized_preconditions"; 50 51 private final Context mAppContext; 52 private final SettingsObserver mSettingsObserver; 53 54 private final Object mLock = new Object(); 55 private final Runnable mOnChangedListener; 56 @Nullable 57 @GuardedBy("mLock") 58 private ActionsModelParams mActionsModelParams; 59 @GuardedBy("mLock") 60 private boolean mParsed = true; 61 ActionsModelParamsSupplier(Context context, @Nullable Runnable onChangedListener)62 public ActionsModelParamsSupplier(Context context, @Nullable Runnable onChangedListener) { 63 final Context appContext = Preconditions.checkNotNull(context).getApplicationContext(); 64 // Some contexts don't have an app context. 65 mAppContext = appContext != null ? appContext : context; 66 mOnChangedListener = onChangedListener == null ? () -> {} : onChangedListener; 67 mSettingsObserver = new SettingsObserver(mAppContext, () -> { 68 synchronized (mLock) { 69 Log.v(TAG, "Settings.Global.TEXT_CLASSIFIER_ACTION_MODEL_PARAMS is updated"); 70 mParsed = true; 71 mOnChangedListener.run(); 72 } 73 }); 74 } 75 76 /** 77 * Returns the parsed actions params or {@link ActionsModelParams#INVALID} if the value is 78 * invalid. 79 */ 80 @Override get()81 public ActionsModelParams get() { 82 synchronized (mLock) { 83 if (mParsed) { 84 mActionsModelParams = parse(mAppContext.getContentResolver()); 85 mParsed = false; 86 } 87 } 88 return mActionsModelParams; 89 } 90 parse(ContentResolver contentResolver)91 private ActionsModelParams parse(ContentResolver contentResolver) { 92 String settingStr = Settings.Global.getString(contentResolver, 93 Settings.Global.TEXT_CLASSIFIER_ACTION_MODEL_PARAMS); 94 if (TextUtils.isEmpty(settingStr)) { 95 return ActionsModelParams.INVALID; 96 } 97 try { 98 KeyValueListParser keyValueListParser = new KeyValueListParser(','); 99 keyValueListParser.setString(settingStr); 100 int version = keyValueListParser.getInt(KEY_REQUIRED_MODEL_VERSION, -1); 101 if (version == -1) { 102 Log.w(TAG, "ActionsModelParams.Parse, invalid model version"); 103 return ActionsModelParams.INVALID; 104 } 105 String locales = keyValueListParser.getString(KEY_REQUIRED_LOCALES, null); 106 if (locales == null) { 107 Log.w(TAG, "ActionsModelParams.Parse, invalid locales"); 108 return ActionsModelParams.INVALID; 109 } 110 String serializedPreconditionsStr = 111 keyValueListParser.getString(KEY_SERIALIZED_PRECONDITIONS, null); 112 if (serializedPreconditionsStr == null) { 113 Log.w(TAG, "ActionsModelParams.Parse, invalid preconditions"); 114 return ActionsModelParams.INVALID; 115 } 116 byte[] serializedPreconditions = 117 Base64.decode(serializedPreconditionsStr, Base64.NO_WRAP); 118 return new ActionsModelParams(version, locales, serializedPreconditions); 119 } catch (Throwable t) { 120 Log.e(TAG, "Invalid TEXT_CLASSIFIER_ACTION_MODEL_PARAMS, ignore", t); 121 } 122 return ActionsModelParams.INVALID; 123 } 124 125 @Override finalize()126 protected void finalize() throws Throwable { 127 try { 128 mAppContext.getContentResolver().unregisterContentObserver(mSettingsObserver); 129 } finally { 130 super.finalize(); 131 } 132 } 133 134 /** 135 * Represents the parsed result. 136 */ 137 public static final class ActionsModelParams { 138 139 public static final ActionsModelParams INVALID = 140 new ActionsModelParams(-1, "", new byte[0]); 141 142 /** 143 * The required model version to apply {@code mSerializedPreconditions}. 144 */ 145 private final int mRequiredModelVersion; 146 147 /** 148 * The required model locales to apply {@code mSerializedPreconditions}. 149 */ 150 private final String mRequiredModelLocales; 151 152 /** 153 * The serialized params that will be applied to the model file, if all requirements are 154 * met. Do not modify. 155 */ 156 private final byte[] mSerializedPreconditions; 157 ActionsModelParams(int requiredModelVersion, String requiredModelLocales, byte[] serializedPreconditions)158 public ActionsModelParams(int requiredModelVersion, String requiredModelLocales, 159 byte[] serializedPreconditions) { 160 mRequiredModelVersion = requiredModelVersion; 161 mRequiredModelLocales = Preconditions.checkNotNull(requiredModelLocales); 162 mSerializedPreconditions = Preconditions.checkNotNull(serializedPreconditions); 163 } 164 165 /** 166 * Returns the serialized preconditions. Returns {@code null} if the the model in use does 167 * not meet all the requirements listed in the {@code ActionsModelParams} or the params 168 * are invalid. 169 */ 170 @Nullable getSerializedPreconditions(ModelFileManager.ModelFile modelInUse)171 public byte[] getSerializedPreconditions(ModelFileManager.ModelFile modelInUse) { 172 if (this == INVALID) { 173 return null; 174 } 175 if (modelInUse.getVersion() != mRequiredModelVersion) { 176 Log.w(TAG, String.format( 177 "Not applying mSerializedPreconditions, required version=%d, actual=%d", 178 mRequiredModelVersion, modelInUse.getVersion())); 179 return null; 180 } 181 if (!Objects.equals(modelInUse.getSupportedLocalesStr(), mRequiredModelLocales)) { 182 Log.w(TAG, String.format( 183 "Not applying mSerializedPreconditions, required locales=%s, actual=%s", 184 mRequiredModelLocales, modelInUse.getSupportedLocalesStr())); 185 return null; 186 } 187 return mSerializedPreconditions; 188 } 189 } 190 191 private static final class SettingsObserver extends ContentObserver { 192 193 private final WeakReference<Runnable> mOnChangedListener; 194 SettingsObserver(Context appContext, Runnable listener)195 SettingsObserver(Context appContext, Runnable listener) { 196 super(null); 197 mOnChangedListener = new WeakReference<>(listener); 198 appContext.getContentResolver().registerContentObserver( 199 Settings.Global.getUriFor(Settings.Global.TEXT_CLASSIFIER_ACTION_MODEL_PARAMS), 200 false /* notifyForDescendants */, 201 this); 202 } 203 onChange(boolean selfChange)204 public void onChange(boolean selfChange) { 205 if (mOnChangedListener.get() != null) { 206 mOnChangedListener.get().run(); 207 } 208 } 209 } 210 } 211