1 /* 2 * Copyright (C) 2021 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 17 package com.google.android.tv.btservices.remote; 18 19 import android.content.Context; 20 import android.os.AsyncTask; 21 import android.os.Environment; 22 import android.os.FileObserver; 23 import android.os.Handler; 24 import android.os.SystemProperties; 25 import android.text.TextUtils; 26 import android.util.Log; 27 import androidx.annotation.Nullable; 28 import com.google.android.tv.btservices.R; 29 import java.io.File; 30 import java.io.FileInputStream; 31 import java.security.DigestInputStream; 32 import java.security.MessageDigest; 33 import java.util.Arrays; 34 import java.util.Collections; 35 import java.util.HashSet; 36 import java.util.List; 37 import java.util.Set; 38 import java.util.TreeSet; 39 import java.util.stream.Collectors; 40 41 /** 42 * For determining the current DFU binary. 43 */ 44 public abstract class DfuProvider { 45 46 private static final String TAG = "Atv.RemoteDfuPrvdr"; 47 48 public interface Listener { onDfuFileAdd()49 void onDfuFileAdd(); 50 } 51 52 private class DfuFileObserver extends FileObserver { 53 DfuFileObserver(String path)54 public DfuFileObserver(String path) { 55 super(path, FileObserver.CLOSE_WRITE | FileObserver.DELETE); 56 } 57 58 @Override onEvent(int event, String path)59 public void onEvent(int event, String path) { 60 if (mContext != null) { 61 mHandler.post(DfuProvider.this::checkExternalStorage); 62 } 63 } 64 } 65 66 private class CheckOnDiskDfuFileTask extends AsyncTask<File[], Integer, File[]> { 67 @Override doInBackground(File[].... params)68 protected File[] doInBackground(File[]... params) { 69 if (params == null || params.length == 0 || params[0] == null) { 70 return new File[]{}; 71 } 72 return Arrays.stream(params[0]) 73 .filter((file) -> { 74 return file.isFile() && file.canRead() && 75 isDfuFileName(file.getName()) && 76 (bypassMd5() || MD5s.contains(md5(file.getAbsolutePath()))); 77 }) 78 .collect(Collectors.toList()) 79 .toArray(new File[]{}); 80 } 81 82 @Override onPostExecute(File[] result)83 protected void onPostExecute(File[] result) { 84 boolean changed = false; 85 TreeSet<DfuBinary> newDfus = new TreeSet<>(); 86 newDfus.addAll(getPackagedBinaries()); 87 for (File file: result) { 88 try { 89 FileInputStream fin = new FileInputStream(file.getAbsolutePath()); 90 // New, pushed binaries have priority over the system image binaries, so we set 91 // 'override' to true. Note that this is for QA testing and validation only. 92 DfuBinary dfu = mFactory.build(fin, true /* override */); 93 newDfus.add(dfu); 94 fin.close(); 95 Log.i(TAG, "Found dfu with version: " + dfu.getVersion()); 96 } catch (Exception e) { 97 Log.e(TAG, "CheckOnDiskDfuFileTask: exception " + e); 98 } 99 } 100 for (DfuBinary bin : newDfus) { 101 if (!mDfus.contains(bin)) { 102 changed = true; 103 break; 104 } 105 } 106 for (DfuBinary bin : mDfus) { 107 if (!newDfus.contains(bin)) { 108 changed = true; 109 break; 110 } 111 } 112 mDfus.clear(); 113 mDfus.addAll(newDfus); 114 if (changed) { 115 mListener.onDfuFileAdd(); 116 } 117 } 118 } 119 120 private final TreeSet<DfuBinary> mDfus = new TreeSet<>(); 121 private final DfuBinary.Factory mFactory; 122 private final Handler mHandler = new Handler(); 123 private final Set<String> MD5s; 124 private final Set<Version> mManualReconnectionVersions; 125 private final Context mContext; 126 private MessageDigest mDigest; 127 private FileObserver mObserver; 128 private Listener mListener; 129 130 // This method provides the DFU binaries that are packaged with the APK. getPackagedBinaries()131 protected abstract List<DfuBinary> getPackagedBinaries(); 132 133 /** 134 * Returns the versions from which an upgrade will cause the connection information stored on 135 * the remote control being erased. After an upgrade from one of these versions the connection 136 * to the remote control would need to be forgotten on the host side and the user needs to 137 * perform pairing again. 138 * 139 * @return A set containing all versions for which the above behavior is to be expected. 140 */ getManualReconnectionVersions()141 public Set<Version> getManualReconnectionVersions() { 142 return mManualReconnectionVersions; 143 } 144 convertStrToVersion(String str)145 private static Version convertStrToVersion(String str) { 146 String[] parts = str.split(" "); 147 int major = Integer.parseInt(parts[0], 16); 148 int minor = Integer.parseInt(parts[1], 16); 149 byte vid = (byte) (Integer.parseInt(parts[2], 16) & 0xff); 150 byte pid = (byte) (Integer.parseInt(parts[3], 16) & 0xff); 151 return new Version(major, minor, vid, pid); 152 } 153 DfuProvider(Context context, Listener listener, DfuBinary.Factory factory)154 public DfuProvider(Context context, Listener listener, DfuBinary.Factory factory) { 155 mContext = context; 156 mListener = listener; 157 mFactory = factory; 158 mDfus.addAll(getPackagedBinaries()); 159 MD5s = Collections.unmodifiableSet(new HashSet<>( 160 Arrays.asList(mContext.getResources().getStringArray(R.array.dfu_binary_md5s)))); 161 162 String[] versionStrs = 163 mContext.getResources().getStringArray(R.array.manual_reconnection_remote_versions); 164 mManualReconnectionVersions = 165 Collections.unmodifiableSet(Arrays.stream(versionStrs) 166 .map(DfuProvider::convertStrToVersion) 167 .collect(Collectors.<Version>toSet())); 168 169 try { 170 mDigest = MessageDigest.getInstance("MD5"); 171 } catch (Exception e) { 172 Log.e(TAG, "error in opening md5 digest: " + e); 173 } 174 checkExternalStorage(); 175 File extDir = Environment.getExternalStorageDirectory(); 176 mObserver = new DfuFileObserver(extDir.getAbsolutePath()); 177 mObserver.startWatching(); 178 } 179 destroy()180 public void destroy() { 181 mObserver.stopWatching(); 182 mObserver = null; 183 } 184 bypassMd5()185 private static boolean bypassMd5() { 186 return !TextUtils.isEmpty(SystemProperties.get("btservices.dfu_bypass_md5", "")); 187 } 188 bypassVendorIdCheck()189 private static boolean bypassVendorIdCheck() { 190 return !TextUtils.isEmpty(SystemProperties.get("btservices.dfu_bypass_vendor_id", "")); 191 } 192 bypassProductIdCheck()193 private static boolean bypassProductIdCheck() { 194 return !TextUtils.isEmpty(SystemProperties.get("btservices.dfu_bypass_product_id", "")); 195 } 196 bypassVersionCheck()197 public boolean bypassVersionCheck() { 198 return bypassMd5(); 199 } 200 md5(String absPath)201 private String md5(String absPath) { 202 if (mDigest == null) { 203 return null; 204 } 205 206 File file = new File(absPath); 207 if (!file.isFile()) { 208 return null; 209 } 210 211 FileInputStream fin; 212 try { 213 fin = new FileInputStream(absPath); 214 } catch (Exception e) { 215 Log.e(TAG, "failed to open file: " + absPath); 216 return null; 217 } 218 mDigest.reset(); 219 220 DigestInputStream dis = new DigestInputStream(fin, mDigest); 221 try { 222 while (dis.available() > 0) { 223 dis.read(); 224 } 225 } catch(Exception e) { 226 Log.e(TAG, "failed to read file: " + absPath); 227 return null; 228 } 229 try { 230 fin.close(); 231 } catch (Exception e) { 232 Log.e(TAG, "failed to close file " + absPath); 233 return null; 234 } 235 236 byte[] digest = mDigest.digest(); 237 StringBuilder sb = new StringBuilder(); 238 for(byte b: digest){ 239 sb.append(String.format("%02x", b & 0xff)); 240 } 241 return sb.toString().toLowerCase(); 242 } 243 244 // TODO: Should be replaced with vendor implementation. isDfuFileName(String fname)245 private static boolean isDfuFileName(String fname) { 246 if (fname == null) 247 return false; 248 fname = fname.toLowerCase(); 249 return fname.endsWith(".bin") && fname.contains("ota"); 250 } 251 checkExternalStorage()252 private void checkExternalStorage() { 253 if (mContext == null) { 254 return; 255 } 256 File extDir = Environment.getExternalStorageDirectory(); 257 if (!extDir.isDirectory()) { 258 return; 259 } 260 new CheckOnDiskDfuFileTask().execute(extDir.listFiles()); 261 } 262 263 /** 264 * Given the device name and the firmware version of the current remote, this method determines 265 * the best possible DFU for this remote or null if there are no suitable DFU binaries. 266 * 267 * @param deviceName The name of the device. 268 * @param version The current version of the device. 269 * @return The best matching DfuBinary or null if there is no suitable one available. 270 */ 271 @Nullable getDfu(String deviceName, Version version)272 public DfuBinary getDfu(String deviceName, Version version) { 273 DfuBinary best = null; 274 for (DfuBinary bin : mDfus) { 275 Version binVersion = bin.getVersion(); 276 if (!bypassVendorIdCheck() && binVersion.vid() != version.vid()) { 277 continue; 278 } 279 280 if (!bypassProductIdCheck() && binVersion.pid() != version.pid()) { 281 continue; 282 } 283 284 if (binVersion.compareTo(version) <= 0) { 285 continue; 286 } 287 288 if (best == null || (best.getVersion().compareTo(binVersion) < 0)) { 289 best = bin; 290 } 291 } 292 return best; 293 } 294 } 295