1 /*
2  * Copyright (C) 2024 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.bluetooth.pbap;
18 
19 import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS;
20 
21 import android.bluetooth.AlertActivity;
22 import android.bluetooth.BluetoothDevice;
23 import android.bluetooth.BluetoothProfile;
24 import android.bluetooth.BluetoothProtoEnums;
25 import android.content.BroadcastReceiver;
26 import android.content.Context;
27 import android.content.DialogInterface;
28 import android.content.Intent;
29 import android.content.IntentFilter;
30 import android.os.Bundle;
31 import android.os.Handler;
32 import android.os.Message;
33 import android.preference.Preference;
34 import android.text.InputFilter;
35 import android.text.InputFilter.LengthFilter;
36 import android.text.TextWatcher;
37 import android.util.Log;
38 import android.view.View;
39 import android.widget.EditText;
40 import android.widget.TextView;
41 
42 import com.android.bluetooth.BluetoothMethodProxy;
43 import com.android.bluetooth.BluetoothStatsLog;
44 import com.android.bluetooth.R;
45 import com.android.bluetooth.content_profiles.ContentProfileErrorReportUtils;
46 import com.android.internal.annotations.VisibleForTesting;
47 
48 /**
49  * PbapActivity shows two dialogues: One for accepting incoming pbap request and the other prompts
50  * the user to enter a session key for authentication with a remote Bluetooth device.
51  */
52 // Next tag value for ContentProfileErrorReportUtils.report(): 1
53 public class BluetoothPbapActivity extends AlertActivity
54         implements Preference.OnPreferenceChangeListener, TextWatcher {
55     private static final String TAG = "BluetoothPbapActivity";
56 
57     private static final int BLUETOOTH_OBEX_AUTHKEY_MAX_LENGTH = 16;
58 
59     @VisibleForTesting static final int DIALOG_YES_NO_AUTH = 1;
60 
61     private static final String KEY_USER_TIMEOUT = "user_timeout";
62 
63     private View mView;
64 
65     private EditText mKeyView;
66 
67     private TextView mMessageView;
68 
69     private String mSessionKey = "";
70 
71     @VisibleForTesting int mCurrentDialog;
72 
73     private boolean mTimeout = false;
74 
75     @VisibleForTesting static final int DISMISS_TIMEOUT_DIALOG = 0;
76 
77     @VisibleForTesting static final long DISMISS_TIMEOUT_DIALOG_DELAY_MS = 2_000;
78 
79     private BluetoothDevice mDevice;
80 
81     @VisibleForTesting
82     BroadcastReceiver mReceiver =
83             new BroadcastReceiver() {
84                 @Override
85                 public void onReceive(Context context, Intent intent) {
86                     if (!BluetoothPbapService.USER_CONFIRM_TIMEOUT_ACTION.equals(
87                             intent.getAction())) {
88                         return;
89                     }
90                     onTimeout();
91                 }
92             };
93 
94     @Override
onCreate(Bundle savedInstanceState)95     protected void onCreate(Bundle savedInstanceState) {
96         super.onCreate(savedInstanceState);
97 
98         getWindow().addSystemFlags(SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS);
99         Intent i = getIntent();
100         String action = i.getAction();
101         mDevice = i.getParcelableExtra(BluetoothPbapService.EXTRA_DEVICE);
102         if (action != null && action.equals(BluetoothPbapService.AUTH_CHALL_ACTION)) {
103             showPbapDialog(DIALOG_YES_NO_AUTH);
104             mCurrentDialog = DIALOG_YES_NO_AUTH;
105         } else {
106             Log.e(
107                     TAG,
108                     "Error: this activity may be started only with intent "
109                             + "PBAP_ACCESS_REQUEST or PBAP_AUTH_CHALL ");
110             ContentProfileErrorReportUtils.report(
111                     BluetoothProfile.PBAP,
112                     BluetoothProtoEnums.BLUETOOTH_PBAP_ACTIVITY,
113                     BluetoothStatsLog.BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__LOG_ERROR,
114                     0);
115             finish();
116         }
117         IntentFilter filter = new IntentFilter(BluetoothPbapService.USER_CONFIRM_TIMEOUT_ACTION);
118         filter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY);
119         registerReceiver(mReceiver, filter);
120     }
121 
showPbapDialog(int id)122     private void showPbapDialog(int id) {
123         switch (id) {
124             case DIALOG_YES_NO_AUTH:
125                 mAlertBuilder.setTitle(getString(R.string.pbap_session_key_dialog_header));
126                 mAlertBuilder.setView(createView(DIALOG_YES_NO_AUTH));
127                 mAlertBuilder.setPositiveButton(
128                         android.R.string.ok, (dialog, which) -> onPositive());
129                 mAlertBuilder.setNegativeButton(
130                         android.R.string.cancel, (dialog, which) -> onNegative());
131                 setupAlert();
132                 changeButtonEnabled(DialogInterface.BUTTON_POSITIVE, false);
133                 break;
134             default:
135                 break;
136         }
137     }
138 
createDisplayText(final int id)139     private String createDisplayText(final int id) {
140         switch (id) {
141             case DIALOG_YES_NO_AUTH:
142                 String mMessage2 = getString(R.string.pbap_session_key_dialog_title, mDevice);
143                 return mMessage2;
144             default:
145                 return null;
146         }
147     }
148 
createView(final int id)149     private View createView(final int id) {
150         switch (id) {
151             case DIALOG_YES_NO_AUTH:
152                 mView = getLayoutInflater().inflate(R.layout.auth, null);
153                 mMessageView = (TextView) mView.findViewById(R.id.message);
154                 mMessageView.setText(createDisplayText(id));
155                 mKeyView = (EditText) mView.findViewById(R.id.text);
156                 mKeyView.addTextChangedListener(this);
157                 mKeyView.setFilters(
158                         new InputFilter[] {new LengthFilter(BLUETOOTH_OBEX_AUTHKEY_MAX_LENGTH)});
159                 return mView;
160             default:
161                 return null;
162         }
163     }
164 
165     @VisibleForTesting
onPositive()166     void onPositive() {
167         if (mCurrentDialog == DIALOG_YES_NO_AUTH) {
168             mSessionKey = mKeyView.getText().toString();
169         }
170 
171         if (!mTimeout) {
172             if (mCurrentDialog == DIALOG_YES_NO_AUTH) {
173                 sendIntentToReceiver(
174                         BluetoothPbapService.AUTH_RESPONSE_ACTION,
175                         BluetoothPbapService.EXTRA_SESSION_KEY,
176                         mSessionKey);
177                 mKeyView.removeTextChangedListener(this);
178             }
179         }
180         mTimeout = false;
181         finish();
182     }
183 
184     @VisibleForTesting
onNegative()185     void onNegative() {
186         if (mCurrentDialog == DIALOG_YES_NO_AUTH) {
187             sendIntentToReceiver(BluetoothPbapService.AUTH_CANCELLED_ACTION, null, null);
188             mKeyView.removeTextChangedListener(this);
189         }
190         finish();
191     }
192 
sendIntentToReceiver( final String intentName, final String extraName, final String extraValue)193     private void sendIntentToReceiver(
194             final String intentName, final String extraName, final String extraValue) {
195         Intent intent = new Intent(intentName);
196         intent.setPackage(getPackageName());
197         intent.putExtra(BluetoothPbapService.EXTRA_DEVICE, mDevice);
198         if (extraName != null) {
199             intent.putExtra(extraName, extraValue);
200         }
201         sendBroadcast(intent);
202     }
203 
204     @VisibleForTesting
onTimeout()205     private void onTimeout() {
206         mTimeout = true;
207         if (mCurrentDialog == DIALOG_YES_NO_AUTH) {
208             mMessageView.setText(getString(R.string.pbap_authentication_timeout_message, mDevice));
209             mKeyView.setVisibility(View.GONE);
210             mKeyView.clearFocus();
211             mKeyView.removeTextChangedListener(this);
212             changeButtonEnabled(DialogInterface.BUTTON_POSITIVE, true);
213             changeButtonVisibility(DialogInterface.BUTTON_NEGATIVE, View.GONE);
214         }
215 
216         BluetoothMethodProxy.getInstance()
217                 .handlerSendMessageDelayed(
218                         mTimeoutHandler, DISMISS_TIMEOUT_DIALOG, DISMISS_TIMEOUT_DIALOG_DELAY_MS);
219     }
220 
221     @Override
onRestoreInstanceState(Bundle savedInstanceState)222     protected void onRestoreInstanceState(Bundle savedInstanceState) {
223         super.onRestoreInstanceState(savedInstanceState);
224         mTimeout = savedInstanceState.getBoolean(KEY_USER_TIMEOUT);
225         Log.v(TAG, "onRestoreInstanceState() mTimeout: " + mTimeout);
226         if (mTimeout) {
227             onTimeout();
228         }
229     }
230 
231     @Override
onSaveInstanceState(Bundle outState)232     protected void onSaveInstanceState(Bundle outState) {
233         super.onSaveInstanceState(outState);
234         outState.putBoolean(KEY_USER_TIMEOUT, mTimeout);
235     }
236 
237     @Override
onDestroy()238     protected void onDestroy() {
239         super.onDestroy();
240         unregisterReceiver(mReceiver);
241     }
242 
243     @Override
onPreferenceChange(Preference preference, Object newValue)244     public boolean onPreferenceChange(Preference preference, Object newValue) {
245         return true;
246     }
247 
248     @Override
beforeTextChanged(CharSequence s, int start, int before, int after)249     public void beforeTextChanged(CharSequence s, int start, int before, int after) {}
250 
251     @Override
onTextChanged(CharSequence s, int start, int before, int count)252     public void onTextChanged(CharSequence s, int start, int before, int count) {}
253 
254     @Override
afterTextChanged(android.text.Editable s)255     public void afterTextChanged(android.text.Editable s) {
256         if (s.length() > 0) {
257             changeButtonEnabled(DialogInterface.BUTTON_POSITIVE, true);
258         }
259     }
260 
261     private final Handler mTimeoutHandler =
262             new Handler() {
263                 @Override
264                 public void handleMessage(Message msg) {
265                     switch (msg.what) {
266                         case DISMISS_TIMEOUT_DIALOG:
267                             Log.v(TAG, "Received DISMISS_TIMEOUT_DIALOG msg.");
268                             finish();
269                             break;
270                         default:
271                             break;
272                     }
273                 }
274             };
275 }
276