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.settings; 18 19 import android.app.Activity; 20 import android.bluetooth.BluetoothDevice; 21 import android.content.ComponentName; 22 import android.content.Context; 23 import android.content.Intent; 24 import android.content.ServiceConnection; 25 import android.os.Bundle; 26 import android.os.Handler; 27 import android.os.IBinder; 28 import android.text.TextUtils; 29 import android.util.Log; 30 import android.view.View; 31 import android.widget.ImageView; 32 import android.widget.ProgressBar; 33 import android.widget.TextView; 34 import com.google.android.tv.btservices.BluetoothDeviceService; 35 import com.google.android.tv.btservices.BluetoothUtils; 36 import com.google.android.tv.btservices.Configuration; 37 import com.google.android.tv.btservices.R; 38 import com.google.android.tv.btservices.remote.DfuManager; 39 import com.google.android.tv.btservices.remote.RemoteProxy.DfuResult; 40 import com.google.android.tv.btservices.SimplifiedConnection; 41 42 public class RemoteDfuActivity extends Activity implements DfuManager.Listener { 43 44 public static final String EXTRA_BT_ADDRESS = "extra_bt_address"; 45 46 private static final String TAG = "Atv.RmtDfuActivity"; 47 48 private static final int BEFORE_FINISH_DELAY_MS = 4000; 49 private static final int PROGRESS_BAR_MAX = 100; 50 private static final int PLEASE_WAIT_TIMEOUT_MS = 5000; 51 52 private final ServiceConnection mBtDeviceServiceConnection = new SimplifiedConnection() { 53 54 @Override 55 protected void cleanUp() { 56 if (mBtDeviceServiceBinder != null) { 57 mBtDeviceServiceBinder.removeListener(RemoteDfuActivity.this); 58 } 59 mBtDeviceServiceBound = false; 60 mBtDeviceServiceBinder = null; 61 } 62 63 @Override 64 public void onServiceConnected(ComponentName className, IBinder service) { 65 mBtDeviceServiceBinder = (BluetoothDeviceService.LocalBinder) service; 66 mBtDeviceServiceBound = true; 67 mBtDeviceServiceBinder.addListener(RemoteDfuActivity.this); 68 mHandler.post(RemoteDfuActivity.this::attemptDfu); 69 } 70 }; 71 72 private BluetoothDevice mDevice; 73 private String mBtAddress; 74 private boolean mBtDeviceServiceBound; 75 private BluetoothDeviceService.LocalBinder mBtDeviceServiceBinder; 76 private final Handler mHandler = new Handler(); 77 private ImageView mRemoteIcon; 78 private TextView mTitleView; 79 private TextView mSummaryView; 80 private ProgressBar mProgressBar; 81 private ImageView mCheckIcon; 82 private Runnable mPleaseWaitTimeout = this::onErrorBeforeUpgradeFinished; 83 84 private static final int NO_UPGRADE_ERR = R.string.settings_bt_update_not_necessary; 85 private static final int UPDATE_ERR = R.string.settings_bt_update_error; 86 private static final int BEFORE_UPDATE_FINISHED_ERR = R.string.settings_bt_update_failed; 87 private static final int LOW_BATTERY_ERR = R.string.settings_bt_battery_low; 88 error(int stringResId)89 private void error(int stringResId) { 90 // No longer care about the "Please wait" state 91 if (mHandler.hasCallbacks(mPleaseWaitTimeout)) { 92 mHandler.removeCallbacks(mPleaseWaitTimeout); 93 } 94 mTitleView.setText(stringResId); 95 Log.w(TAG, "error: " + getString(stringResId)); 96 mSummaryView.setVisibility(View.GONE); 97 mProgressBar.setVisibility(View.GONE); 98 mHandler.postDelayed(this::finish, BEFORE_FINISH_DELAY_MS); 99 } 100 onErrorBeforeUpgradeFinished()101 private void onErrorBeforeUpgradeFinished() { 102 error(BEFORE_UPDATE_FINISHED_ERR); 103 } 104 onDone()105 private void onDone() { 106 // We've recovered from the "Please wait" state. 107 if (mHandler.hasCallbacks(mPleaseWaitTimeout)) { 108 mHandler.removeCallbacks(mPleaseWaitTimeout); 109 } 110 111 mProgressBar.setVisibility(View.VISIBLE); 112 mCheckIcon.setVisibility(View.VISIBLE); 113 mHandler.postDelayed(this::finish, BEFORE_FINISH_DELAY_MS); 114 } 115 onDoneNeedsPairing()116 private void onDoneNeedsPairing() { 117 // We've recovered from the "Please wait" state. 118 if (mHandler.hasCallbacks(mPleaseWaitTimeout)) { 119 mHandler.removeCallbacks(mPleaseWaitTimeout); 120 } 121 122 Runnable showDone = () -> { 123 mProgressBar.setVisibility(View.VISIBLE); 124 mCheckIcon.setVisibility(View.VISIBLE); 125 }; 126 Runnable showPairingNeeded = () -> { 127 mProgressBar.setVisibility(View.INVISIBLE); 128 mCheckIcon.setVisibility(View.INVISIBLE); 129 mSummaryView.setVisibility(View.INVISIBLE); 130 mTitleView.setVisibility(View.VISIBLE); 131 mTitleView.setText(R.string.settings_bt_update_needs_repair); 132 }; 133 Runnable startPairingAndFinish = () -> { 134 BluetoothDeviceService.forgetAndRepair(this, mDevice); 135 finish(); 136 }; 137 138 final long beforePairingMsgMs = BEFORE_FINISH_DELAY_MS; 139 mHandler.post(showDone); 140 mHandler.postDelayed(showPairingNeeded, beforePairingMsgMs); 141 mHandler.postDelayed(startPairingAndFinish, beforePairingMsgMs + BEFORE_FINISH_DELAY_MS); 142 } 143 onProgress(double progress)144 private void onProgress(double progress) { 145 // We've recovered from the "Please wait" state. 146 if (mHandler.hasCallbacks(mPleaseWaitTimeout)) { 147 mHandler.removeCallbacks(mPleaseWaitTimeout); 148 } 149 150 mProgressBar.setVisibility(View.VISIBLE); 151 mProgressBar.setProgress((int) Math.round(progress * PROGRESS_BAR_MAX)); 152 } 153 154 onUnknownState()155 private void onUnknownState() { 156 // We've entered an unknown state. It could be that update has failed. In this case, post a 157 // "Please wait" message to the user and then timeout. 158 mTitleView.setText(R.string.settings_bt_update_please_wait); 159 mSummaryView.setVisibility(View.GONE); 160 mHandler.removeCallbacks(mPleaseWaitTimeout); 161 mHandler.postDelayed(mPleaseWaitTimeout, PLEASE_WAIT_TIMEOUT_MS); 162 } 163 attemptDfu()164 private void attemptDfu() { 165 if (mBtDeviceServiceBinder == null) { 166 error(UPDATE_ERR); 167 return; 168 } 169 170 if (TextUtils.isEmpty(mBtAddress)) { 171 Log.e(TAG, "bt address is empty"); 172 error(UPDATE_ERR); 173 return; 174 } 175 176 if (!mBtDeviceServiceBinder.hasUpgrade(mDevice)) { 177 error(NO_UPGRADE_ERR); 178 return; 179 } 180 181 if (mBtDeviceServiceBinder.isBatteryLow(mDevice)) { 182 error(LOW_BATTERY_ERR); 183 return; 184 } 185 mBtDeviceServiceBinder.startDfu(mDevice); 186 } 187 188 @Override onCreate(Bundle savedInstanceState)189 protected void onCreate(Bundle savedInstanceState) { 190 super.onCreate(savedInstanceState); 191 setContentView(R.layout.remote_dfu); 192 193 mRemoteIcon = findViewById(R.id.icon); 194 mTitleView = findViewById(R.id.fullscreen_title); 195 mSummaryView = findViewById(R.id.fullscreen_summary); 196 mProgressBar = findViewById(R.id.fullscreen_progressbar); 197 mCheckIcon = findViewById(R.id.check_icon); 198 mProgressBar.setMin(0); 199 mProgressBar.setMax(PROGRESS_BAR_MAX); 200 201 mRemoteIcon.setClipToOutline(false); 202 if (!showRemoteControlIcon()) { 203 mRemoteIcon.setVisibility(View.INVISIBLE); 204 } 205 206 Intent intent = getIntent(); 207 mBtAddress = intent.getStringExtra(EXTRA_BT_ADDRESS); 208 mDevice = BluetoothDeviceService.findDevice(mBtAddress); 209 210 if (TextUtils.isEmpty(mBtAddress)) { 211 error(UPDATE_ERR); 212 return; 213 } 214 if (!mBtDeviceServiceBound) { 215 bindService(new Intent(this, BluetoothUtils.getBluetoothDeviceServiceClass(this)), 216 mBtDeviceServiceConnection, Context.BIND_AUTO_CREATE); 217 } 218 } 219 220 @Override onDestroy()221 public void onDestroy() { 222 if (mBtDeviceServiceBound) { 223 mBtDeviceServiceBinder.removeListener(this); 224 unbindService(mBtDeviceServiceConnection); 225 } 226 super.onDestroy(); 227 } 228 229 // DfuManager.Listener implementation 230 @Override onDfuProgress(BluetoothDevice device, DfuResult state)231 public void onDfuProgress(BluetoothDevice device, DfuResult state) { 232 if (!TextUtils.equals(mBtAddress, device.getAddress())) { 233 return; 234 } 235 236 if (state == null) { 237 Log.e(TAG, "unknown state"); 238 mHandler.post(this::onUnknownState); 239 return; 240 } 241 242 final double progress = state.progress(); 243 switch (state.code()) { 244 case DfuResult.SUCCESS: 245 Log.i(TAG, "onDfuProgress: success"); 246 mHandler.post(this::onDone); 247 break; 248 case DfuResult.SUCCESS_NEEDS_PAIRING: 249 Log.i(TAG, "onDfuProgress: success needs pairing"); 250 mHandler.post(this::onDoneNeedsPairing); 251 break; 252 case DfuResult.IN_PROGRESS: 253 mHandler.post(() -> onProgress(progress)); 254 break; 255 case DfuResult.UNKNOWN_FAILURE: 256 case DfuResult.GATT_DISCONNECTED: 257 Log.i(TAG, "onDfuProgress: other=" + state.code()); 258 default: 259 mHandler.post(this::onErrorBeforeUpgradeFinished); 260 break; 261 } 262 } 263 showRemoteControlIcon()264 private boolean showRemoteControlIcon() { 265 return Configuration.get(this).isEnabled(R.bool.show_remote_icon_in_dfu); 266 } 267 } 268