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