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