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