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 17 package com.android.car.companiondevicesupport.activity; 18 19 import static com.android.car.connecteddevice.util.SafeLog.logd; 20 import static com.android.car.connecteddevice.util.SafeLog.loge; 21 22 import android.annotation.NonNull; 23 import android.app.AlertDialog; 24 import android.app.Dialog; 25 import android.bluetooth.BluetoothAdapter; 26 import android.content.DialogInterface; 27 import android.content.Intent; 28 import android.os.Bundle; 29 import android.text.Html; 30 import android.text.Spanned; 31 import android.widget.Toast; 32 33 import androidx.fragment.app.DialogFragment; 34 import androidx.fragment.app.Fragment; 35 import androidx.fragment.app.FragmentActivity; 36 import androidx.lifecycle.ViewModelProviders; 37 38 import com.android.car.companiondevicesupport.R; 39 import com.android.car.companiondevicesupport.api.external.AssociatedDevice; 40 import com.android.car.ui.toolbar.MenuItem; 41 import com.android.car.ui.toolbar.Toolbar; 42 43 import java.util.Arrays; 44 45 /** Activity class for association */ 46 public class AssociationActivity extends FragmentActivity { 47 48 /** Intent action used to request a device be associated.*/ 49 public static final String ACTION_ASSOCIATION_SETTING = 50 "com.android.car.companiondevicesupport.ASSOCIATION_ACTIVITY"; 51 52 /** Data name for associated device. */ 53 public static final String ASSOCIATED_DEVICE_DATA_NAME_EXTRA = 54 "com.android.car.companiondevicesupport.ASSOCIATED_DEVICE"; 55 56 private static final String TAG = "CompanionAssociationActivity"; 57 private static final String ADD_DEVICE_FRAGMENT_TAG = "AddAssociatedDeviceFragment"; 58 private static final String DEVICE_DETAIL_FRAGMENT_TAG = "AssociatedDeviceDetailFragment"; 59 private static final String PAIRING_CODE_FRAGMENT_TAG = "ConfirmPairingCodeFragment"; 60 private static final String TURN_ON_BLUETOOTH_FRAGMENT_TAG = "TurnOnBluetoothFragment"; 61 private static final String ASSOCIATION_ERROR_FRAGMENT_TAG = "AssociationErrorFragment"; 62 private static final String REMOVE_DEVICE_DIALOG_TAG = "RemoveDeviceDialog"; 63 private static final String TURN_ON_BLUETOOTH_DIALOG_TAG = "TurnOnBluetoothDialog"; 64 65 private Toolbar mToolbar; 66 private AssociatedDeviceViewModel mModel; 67 68 @Override onCreate(Bundle saveInstanceState)69 public void onCreate(Bundle saveInstanceState) { 70 super.onCreate(saveInstanceState); 71 setContentView(R.layout.base_activity); 72 mToolbar = findViewById(R.id.toolbar); 73 observeViewModel(); 74 if (saveInstanceState != null) { 75 resumePreviousState(); 76 } 77 mToolbar.showProgressBar(); 78 } 79 80 @Override onBackPressed()81 public void onBackPressed() { 82 super.onBackPressed(); 83 mModel.stopAssociation(); 84 dismissConfirmButtons(); 85 mToolbar.hideProgressBar(); 86 } 87 observeViewModel()88 private void observeViewModel() { 89 mModel = ViewModelProviders.of(this).get(AssociatedDeviceViewModel.class); 90 91 mModel.getAssociationState().observe(this, state -> { 92 switch (state) { 93 case PENDING: 94 if (!BluetoothAdapter.getDefaultAdapter().isEnabled()) { 95 runOnUiThread(this::showTurnOnBluetoothFragment); 96 } 97 break; 98 case COMPLETED: 99 mModel.resetAssociationState(); 100 runOnUiThread(() -> Toast.makeText(getApplicationContext(), 101 getString(R.string.continue_setup_toast_text), 102 Toast.LENGTH_SHORT).show()); 103 break; 104 case ERROR: 105 mModel.resetAssociationState(); 106 runOnUiThread(this::showAssociationErrorFragment); 107 break; 108 case NONE: 109 case STARTING: 110 case STARTED: 111 break; 112 default: 113 loge(TAG, "Encountered unexpected association state: " + state); 114 } 115 }); 116 117 mModel.getBluetoothState().observe(this, state -> { 118 if (state != BluetoothAdapter.STATE_ON && getSupportFragmentManager() 119 .findFragmentByTag(DEVICE_DETAIL_FRAGMENT_TAG) != null) { 120 runOnUiThread(this::showTurnOnBluetoothDialog); 121 } 122 }); 123 124 mModel.getAdvertisedCarName().observe(this, name -> { 125 if (name != null) { 126 runOnUiThread(() -> showAddAssociatedDeviceFragment(name)); 127 } 128 }); 129 mModel.getPairingCode().observe(this, code -> { 130 if (code != null) { 131 runOnUiThread(() -> showConfirmPairingCodeFragment(code)); 132 } 133 }); 134 mModel.getDeviceDetails().observe(this, deviceDetails -> { 135 if (deviceDetails == null) { 136 return; 137 } 138 AssociatedDevice device = deviceDetails.getAssociatedDevice(); 139 if (isStartedByFeature()) { 140 setDeviceToReturn(device); 141 } else { 142 runOnUiThread(this::showAssociatedDeviceDetailFragment); 143 } 144 }); 145 146 mModel.getDeviceToRemove().observe(this, device -> { 147 if (device != null) { 148 runOnUiThread(() -> showRemoveDeviceDialog(device)); 149 } 150 }); 151 152 mModel.getRemovedDevice().observe(this, device -> { 153 if (device != null) { 154 runOnUiThread(() -> Toast.makeText(getApplicationContext(), 155 getString(R.string.device_removed_success_toast_text, 156 device.getDeviceName()), 157 Toast.LENGTH_SHORT).show()); 158 finish(); 159 } 160 }); 161 mModel.isFinished().observe(this, isFinished -> { 162 if (isFinished) { 163 finish(); 164 } 165 }); 166 } 167 showTurnOnBluetoothFragment()168 private void showTurnOnBluetoothFragment() { 169 TurnOnBluetoothFragment fragment = new TurnOnBluetoothFragment(); 170 mToolbar.showProgressBar(); 171 launchFragment(fragment, TURN_ON_BLUETOOTH_FRAGMENT_TAG); 172 } 173 showAddAssociatedDeviceFragment(String deviceName)174 private void showAddAssociatedDeviceFragment(String deviceName) { 175 AddAssociatedDeviceFragment fragment = AddAssociatedDeviceFragment.newInstance(deviceName); 176 launchFragment(fragment, ADD_DEVICE_FRAGMENT_TAG); 177 mToolbar.showProgressBar(); 178 } 179 showConfirmPairingCodeFragment(String pairingCode)180 private void showConfirmPairingCodeFragment(String pairingCode) { 181 ConfirmPairingCodeFragment fragment = ConfirmPairingCodeFragment.newInstance(pairingCode); 182 launchFragment(fragment, PAIRING_CODE_FRAGMENT_TAG); 183 showConfirmButtons(); 184 mToolbar.hideProgressBar(); 185 } 186 showAssociationErrorFragment()187 private void showAssociationErrorFragment() { 188 dismissConfirmButtons(); 189 mToolbar.showProgressBar(); 190 AssociationErrorFragment fragment = new AssociationErrorFragment(); 191 launchFragment(fragment, ASSOCIATION_ERROR_FRAGMENT_TAG); 192 } 193 showAssociatedDeviceDetailFragment()194 private void showAssociatedDeviceDetailFragment() { 195 AssociatedDeviceDetailFragment fragment = new AssociatedDeviceDetailFragment(); 196 launchFragment(fragment, DEVICE_DETAIL_FRAGMENT_TAG); 197 mToolbar.hideProgressBar(); 198 showTurnOnBluetoothDialog(); 199 } 200 showConfirmButtons()201 private void showConfirmButtons() { 202 MenuItem cancelButton = MenuItem.builder(this) 203 .setTitle(R.string.retry) 204 .setOnClickListener(i -> retryAssociation()) 205 .build(); 206 MenuItem confirmButton = MenuItem.builder(this) 207 .setTitle(R.string.confirm) 208 .setOnClickListener(i -> { 209 mModel.acceptVerification(); 210 dismissConfirmButtons(); 211 }) 212 .build(); 213 mToolbar.setMenuItems(Arrays.asList(cancelButton, confirmButton)); 214 } 215 dismissConfirmButtons()216 private void dismissConfirmButtons() { 217 mToolbar.setMenuItems(null); 218 } 219 showRemoveDeviceDialog(AssociatedDevice device)220 private void showRemoveDeviceDialog(AssociatedDevice device) { 221 RemoveDeviceDialogFragment removeDeviceDialogFragment = 222 RemoveDeviceDialogFragment.newInstance(device.getDeviceName(), 223 (d, which) -> mModel.removeCurrentDevice()); 224 removeDeviceDialogFragment.show(getSupportFragmentManager(), REMOVE_DEVICE_DIALOG_TAG); 225 } 226 showTurnOnBluetoothDialog()227 private void showTurnOnBluetoothDialog() { 228 if (!BluetoothAdapter.getDefaultAdapter().isEnabled()) { 229 TurnOnBluetoothDialogFragment fragment = new TurnOnBluetoothDialogFragment(); 230 fragment.show(getSupportFragmentManager(), TURN_ON_BLUETOOTH_DIALOG_TAG); 231 } 232 } 233 resumePreviousState()234 private void resumePreviousState() { 235 if (getSupportFragmentManager().findFragmentByTag(PAIRING_CODE_FRAGMENT_TAG) != null) { 236 showConfirmButtons(); 237 } 238 239 RemoveDeviceDialogFragment removeDeviceDialogFragment = 240 (RemoveDeviceDialogFragment) getSupportFragmentManager() 241 .findFragmentByTag(REMOVE_DEVICE_DIALOG_TAG); 242 if (removeDeviceDialogFragment != null) { 243 removeDeviceDialogFragment.setOnConfirmListener((d, which) -> 244 mModel.removeCurrentDevice()); 245 } 246 } 247 launchFragment(Fragment fragment, String tag)248 private void launchFragment(Fragment fragment, String tag) { 249 getSupportFragmentManager() 250 .beginTransaction() 251 .replace(R.id.fragment_container, fragment, tag) 252 .commit(); 253 } 254 retryAssociation()255 private void retryAssociation() { 256 dismissConfirmButtons(); 257 mToolbar.showProgressBar(); 258 Fragment fragment = getSupportFragmentManager() 259 .findFragmentByTag(PAIRING_CODE_FRAGMENT_TAG); 260 if (fragment != null) { 261 getSupportFragmentManager().beginTransaction().remove(fragment).commit(); 262 } 263 mModel.retryAssociation(); 264 } 265 setDeviceToReturn(AssociatedDevice device)266 private void setDeviceToReturn(AssociatedDevice device) { 267 if (!isStartedByFeature()) { 268 return; 269 } 270 Intent intent = new Intent(); 271 intent.putExtra(ASSOCIATED_DEVICE_DATA_NAME_EXTRA, device); 272 setResult(RESULT_OK, intent); 273 finish(); 274 } 275 isStartedByFeature()276 private boolean isStartedByFeature() { 277 String action = getIntent().getAction(); 278 return ACTION_ASSOCIATION_SETTING.equals(action); 279 } 280 281 /** Dialog fragment to confirm removing an associated device. */ 282 public static class RemoveDeviceDialogFragment extends DialogFragment { 283 private static final String DEVICE_NAME_KEY = "device_name"; 284 285 private DialogInterface.OnClickListener mOnConfirmListener; 286 newInstance(@onNull String deviceName, DialogInterface.OnClickListener listener)287 static RemoveDeviceDialogFragment newInstance(@NonNull String deviceName, 288 DialogInterface.OnClickListener listener) { 289 Bundle bundle = new Bundle(); 290 bundle.putString(DEVICE_NAME_KEY, deviceName); 291 RemoveDeviceDialogFragment fragment = new RemoveDeviceDialogFragment(); 292 fragment.setArguments(bundle); 293 fragment.setOnConfirmListener(listener); 294 return fragment; 295 } 296 297 @Override onCreateDialog(Bundle savedInstanceState)298 public Dialog onCreateDialog(Bundle savedInstanceState) { 299 Bundle bundle = getArguments(); 300 String deviceName = bundle.getString(DEVICE_NAME_KEY); 301 String title = getString(R.string.remove_associated_device_title, deviceName); 302 Spanned styledTitle = Html.fromHtml(title, Html.FROM_HTML_MODE_LEGACY); 303 return new AlertDialog.Builder(getActivity()) 304 .setTitle(styledTitle) 305 .setMessage(getString(R.string.remove_associated_device_message)) 306 .setNegativeButton(getString(R.string.cancel), null) 307 .setPositiveButton(getString(R.string.forget), mOnConfirmListener) 308 .setCancelable(true) 309 .create(); 310 } 311 setOnConfirmListener(DialogInterface.OnClickListener onConfirmListener)312 void setOnConfirmListener(DialogInterface.OnClickListener onConfirmListener) { 313 mOnConfirmListener = onConfirmListener; 314 } 315 } 316 317 public static class TurnOnBluetoothDialogFragment extends DialogFragment { 318 @Override onCreateDialog(Bundle savedInstanceState)319 public Dialog onCreateDialog(Bundle savedInstanceState) { 320 return new AlertDialog.Builder(getActivity()) 321 .setTitle(getString(R.string.turn_on_bluetooth_dialog_title)) 322 .setMessage(getString(R.string.turn_on_bluetooth_dialog_message)) 323 .setPositiveButton(getString(R.string.turn_on), (d, w) -> 324 BluetoothAdapter.getDefaultAdapter().enable()) 325 .setNegativeButton(getString(R.string.not_now), null) 326 .setCancelable(true) 327 .create(); 328 } 329 } 330 } 331