1 /* 2 * Copyright (c) 2008-2009, Motorola, Inc. 3 * 4 * All rights reserved. 5 * 6 * Redistribution and use in source and binary forms, with or without 7 * modification, are permitted provided that the following conditions are met: 8 * 9 * - Redistributions of source code must retain the above copyright notice, 10 * this list of conditions and the following disclaimer. 11 * 12 * - Redistributions in binary form must reproduce the above copyright notice, 13 * this list of conditions and the following disclaimer in the documentation 14 * and/or other materials provided with the distribution. 15 * 16 * - Neither the name of the Motorola, Inc. nor the names of its contributors 17 * may be used to endorse or promote products derived from this software 18 * without specific prior written permission. 19 * 20 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 23 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 24 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 25 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 26 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 27 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 28 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 29 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 30 * POSSIBILITY OF SUCH DAMAGE. 31 */ 32 33 package com.android.bluetooth.opp; 34 35 import android.app.Activity; 36 import android.app.AlertDialog; 37 import android.bluetooth.BluetoothAdapter; 38 import android.bluetooth.BluetoothProfile; 39 import android.bluetooth.BluetoothProtoEnums; 40 import android.content.DialogInterface; 41 import android.content.Intent; 42 import android.database.Cursor; 43 import android.database.StaleDataException; 44 import android.net.Uri; 45 import android.os.Bundle; 46 import android.util.Log; 47 import android.view.ContextMenu; 48 import android.view.ContextMenu.ContextMenuInfo; 49 import android.view.Menu; 50 import android.view.MenuInflater; 51 import android.view.MenuItem; 52 import android.view.View; 53 import android.widget.AdapterView; 54 import android.widget.AdapterView.OnItemClickListener; 55 import android.widget.ListView; 56 57 import androidx.core.graphics.Insets; 58 import androidx.core.view.ViewCompat; 59 import androidx.core.view.WindowInsetsCompat; 60 61 import com.android.bluetooth.BluetoothMethodProxy; 62 import com.android.bluetooth.BluetoothStatsLog; 63 import com.android.bluetooth.R; 64 import com.android.bluetooth.content_profiles.ContentProfileErrorReportUtils; 65 import com.android.bluetooth.flags.Flags; 66 67 /** 68 * View showing the user's finished bluetooth opp transfers that the user does not confirm. 69 * Including outbound and inbound transfers, both successful and failed. 70 */ 71 // Next tag value for ContentProfileErrorReportUtils.report(): 2 72 public class BluetoothOppTransferHistory extends Activity 73 implements View.OnCreateContextMenuListener, OnItemClickListener { 74 private static final String TAG = BluetoothOppTransferHistory.class.getSimpleName(); 75 76 private ListView mListView; 77 78 private Cursor mTransferCursor; 79 80 private BluetoothOppTransferAdapter mTransferAdapter; 81 82 private int mIdColumnId; 83 84 private int mContextMenuPosition; 85 86 private boolean mContextMenu = false; 87 88 /** Class to handle Notification Manager updates */ 89 private BluetoothOppNotification mNotifier; 90 91 @Override onCreate(Bundle icicle)92 public void onCreate(Bundle icicle) { 93 super.onCreate(icicle); 94 95 if (Flags.oppSetInsetsForEdgeToEdge()) { 96 ViewCompat.setOnApplyWindowInsetsListener( 97 findViewById(android.R.id.content), 98 (v, windowInsets) -> { 99 Insets insets = 100 windowInsets.getInsets( 101 WindowInsetsCompat.Type.systemBars() 102 | WindowInsetsCompat.Type.ime() 103 | WindowInsetsCompat.Type.displayCutout()); 104 v.setPadding(insets.left, insets.top, insets.right, insets.bottom); 105 return WindowInsetsCompat.CONSUMED; 106 }); 107 } else { 108 // TODO(b/309578419): Make this activity handle insets properly and then remove this. 109 getTheme().applyStyle(R.style.OptOutEdgeToEdgeEnforcement, /* force */ false); 110 } 111 112 setContentView(R.layout.bluetooth_transfers_page); 113 mListView = (ListView) findViewById(R.id.list); 114 mListView.setEmptyView(findViewById(R.id.empty)); 115 116 boolean isOutbound = 117 Constants.ACTION_OPEN_OUTBOUND_TRANSFER.equals(getIntent().getAction()); 118 119 String direction; 120 if (isOutbound) { 121 setTitle(getText(R.string.outbound_history_title)); 122 direction = 123 "(" 124 + BluetoothShare.DIRECTION 125 + " == " 126 + BluetoothShare.DIRECTION_OUTBOUND 127 + ")"; 128 } else { 129 setTitle(getText(R.string.inbound_history_title)); 130 direction = 131 "(" 132 + BluetoothShare.DIRECTION 133 + " == " 134 + BluetoothShare.DIRECTION_INBOUND 135 + ")"; 136 } 137 138 String selection = 139 BluetoothShare.STATUS 140 + " >= '200' AND " 141 + direction 142 + " AND (" 143 + BluetoothShare.VISIBILITY 144 + " IS NULL OR " 145 + BluetoothShare.VISIBILITY 146 + " == '" 147 + BluetoothShare.VISIBILITY_VISIBLE 148 + "')"; 149 150 final String sortOrder = BluetoothShare.TIMESTAMP + " DESC"; 151 mTransferCursor = 152 BluetoothMethodProxy.getInstance() 153 .contentResolverQuery( 154 getContentResolver(), 155 BluetoothShare.CONTENT_URI, 156 new String[] { 157 "_id", 158 BluetoothShare.FILENAME_HINT, 159 BluetoothShare.STATUS, 160 BluetoothShare.TOTAL_BYTES, 161 BluetoothShare._DATA, 162 BluetoothShare.TIMESTAMP, 163 BluetoothShare.VISIBILITY, 164 BluetoothShare.DESTINATION, 165 BluetoothShare.DIRECTION 166 }, 167 selection, 168 null, 169 sortOrder); 170 171 // only attach everything to the listbox if we can access 172 // the transfer database. Otherwise, just show it empty 173 if (mTransferCursor != null) { 174 mIdColumnId = mTransferCursor.getColumnIndexOrThrow(BluetoothShare._ID); 175 // Create a list "controller" for the data 176 mTransferAdapter = 177 new BluetoothOppTransferAdapter( 178 this, R.layout.bluetooth_transfer_item, mTransferCursor); 179 mListView.setAdapter(mTransferAdapter); 180 mListView.setScrollBarStyle(View.SCROLLBARS_INSIDE_INSET); 181 mListView.setOnCreateContextMenuListener(this); 182 mListView.setOnItemClickListener(this); 183 } 184 185 mNotifier = new BluetoothOppNotification(this); 186 mContextMenu = false; 187 } 188 189 @Override onCreateOptionsMenu(Menu menu)190 public boolean onCreateOptionsMenu(Menu menu) { 191 if (mTransferCursor != null) { 192 MenuInflater inflater = getMenuInflater(); 193 inflater.inflate(R.menu.transferhistory, menu); 194 } 195 return true; 196 } 197 198 @Override onPrepareOptionsMenu(Menu menu)199 public boolean onPrepareOptionsMenu(Menu menu) { 200 menu.findItem(R.id.transfer_menu_clear_all).setEnabled(isTransferComplete()); 201 return super.onPrepareOptionsMenu(menu); 202 } 203 204 @Override onOptionsItemSelected(MenuItem item)205 public boolean onOptionsItemSelected(MenuItem item) { 206 if (item.getItemId() == R.id.transfer_menu_clear_all) { 207 promptClearList(); 208 return true; 209 } 210 return false; 211 } 212 213 @Override onContextItemSelected(MenuItem item)214 public boolean onContextItemSelected(MenuItem item) { 215 if (mTransferCursor.getCount() == 0) { 216 Log.i(TAG, "History is already cleared, not clearing again"); 217 return true; 218 } 219 mTransferCursor.moveToPosition(mContextMenuPosition); 220 if (item.getItemId() == R.id.transfer_menu_open) { 221 openCompleteTransfer(); 222 updateNotificationWhenBtDisabled(); 223 return true; 224 } 225 226 if (item.getItemId() == R.id.transfer_menu_clear) { 227 int sessionId = mTransferCursor.getInt(mIdColumnId); 228 Uri contentUri = Uri.parse(BluetoothShare.CONTENT_URI + "/" + sessionId); 229 BluetoothOppUtility.updateVisibilityToHidden(this, contentUri); 230 updateNotificationWhenBtDisabled(); 231 return true; 232 } 233 return false; 234 } 235 236 @Override onDestroy()237 protected void onDestroy() { 238 if (mTransferCursor != null) { 239 mTransferCursor.close(); 240 } 241 super.onDestroy(); 242 } 243 244 @Override onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo)245 public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { 246 if (mTransferCursor != null) { 247 mContextMenu = true; 248 AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuInfo; 249 mTransferCursor.moveToPosition(info.position); 250 mContextMenuPosition = info.position; 251 252 String fileName = 253 mTransferCursor.getString( 254 mTransferCursor.getColumnIndexOrThrow(BluetoothShare.FILENAME_HINT)); 255 if (fileName == null) { 256 fileName = this.getString(R.string.unknown_file); 257 } 258 menu.setHeaderTitle(fileName); 259 getMenuInflater().inflate(R.menu.transferhistorycontextfinished, menu); 260 } 261 } 262 263 /** Prompt the user if they would like to clear the transfer history */ promptClearList()264 private void promptClearList() { 265 new AlertDialog.Builder(this) 266 .setTitle(R.string.transfer_clear_dlg_title) 267 .setMessage(R.string.transfer_clear_dlg_msg) 268 .setPositiveButton( 269 android.R.string.ok, 270 new DialogInterface.OnClickListener() { 271 @Override 272 public void onClick(DialogInterface dialog, int whichButton) { 273 clearAllDownloads(); 274 } 275 }) 276 .setNegativeButton(android.R.string.cancel, null) 277 .show(); 278 } 279 280 /** Returns true if the device has finished transfers, including error and success. */ isTransferComplete()281 private boolean isTransferComplete() { 282 if (mTransferCursor == null) { 283 return false; 284 } 285 try { 286 if (mTransferCursor.moveToFirst()) { 287 while (!mTransferCursor.isAfterLast()) { 288 int statusColumnId = 289 mTransferCursor.getColumnIndexOrThrow(BluetoothShare.STATUS); 290 int status = mTransferCursor.getInt(statusColumnId); 291 if (BluetoothShare.isStatusCompleted(status)) { 292 return true; 293 } 294 mTransferCursor.moveToNext(); 295 } 296 } 297 } catch (StaleDataException e) { 298 ContentProfileErrorReportUtils.report( 299 BluetoothProfile.OPP, 300 BluetoothProtoEnums.BLUETOOTH_OPP_TRANSFER_HISTORY, 301 BluetoothStatsLog.BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__EXCEPTION, 302 0); 303 } 304 return false; 305 } 306 307 /** Clear all finished transfers, error and success transfer items. */ clearAllDownloads()308 private void clearAllDownloads() { 309 if (mTransferCursor.moveToFirst()) { 310 while (!mTransferCursor.isAfterLast()) { 311 int sessionId = mTransferCursor.getInt(mIdColumnId); 312 Uri contentUri = Uri.parse(BluetoothShare.CONTENT_URI + "/" + sessionId); 313 BluetoothOppUtility.updateVisibilityToHidden(this, contentUri); 314 315 mTransferCursor.moveToNext(); 316 } 317 updateNotificationWhenBtDisabled(); 318 } 319 } 320 321 /* 322 * (non-Javadoc) 323 * @see 324 * android.widget.AdapterView.OnItemClickListener#onItemClick(android.widget 325 * .AdapterView, android.view.View, int, long) 326 */ 327 @Override onItemClick(AdapterView<?> parent, View view, int position, long id)328 public void onItemClick(AdapterView<?> parent, View view, int position, long id) { 329 // Open the selected item 330 Log.v(TAG, "onItemClick: ContextMenu = " + mContextMenu); 331 if (!mContextMenu) { 332 mTransferCursor.moveToPosition(position); 333 openCompleteTransfer(); 334 updateNotificationWhenBtDisabled(); 335 } 336 mContextMenu = false; 337 } 338 339 /** 340 * Open the selected finished transfer. mDownloadCursor must be moved to appropriate position 341 * before calling this function 342 */ openCompleteTransfer()343 private void openCompleteTransfer() { 344 int sessionId = mTransferCursor.getInt(mIdColumnId); 345 Uri contentUri = Uri.parse(BluetoothShare.CONTENT_URI + "/" + sessionId); 346 BluetoothOppTransferInfo transInfo = BluetoothOppUtility.queryRecord(this, contentUri); 347 if (transInfo == null) { 348 Log.e(TAG, "Error: Can not get data from db"); 349 ContentProfileErrorReportUtils.report( 350 BluetoothProfile.OPP, 351 BluetoothProtoEnums.BLUETOOTH_OPP_TRANSFER_HISTORY, 352 BluetoothStatsLog.BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__LOG_ERROR, 353 1); 354 return; 355 } 356 if (transInfo.mDirection == BluetoothShare.DIRECTION_INBOUND 357 && BluetoothShare.isStatusSuccess(transInfo.mStatus)) { 358 // if received file successfully, open this file 359 BluetoothOppUtility.updateVisibilityToHidden(this, contentUri); 360 BluetoothOppUtility.openReceivedFile( 361 this, 362 transInfo.mFileName, 363 transInfo.mFileType, 364 transInfo.mTimeStamp, 365 contentUri); 366 } else { 367 Intent in = new Intent(this, BluetoothOppTransferActivity.class); 368 in.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 369 in.setData(contentUri.normalizeScheme()); 370 this.startActivity(in); 371 } 372 } 373 374 /** 375 * When Bluetooth is disabled, notification can not be updated by ContentObserver in OppService, 376 * so need update manually. 377 */ updateNotificationWhenBtDisabled()378 private void updateNotificationWhenBtDisabled() { 379 BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter(); 380 if (!adapter.isEnabled()) { 381 Log.v(TAG, "Bluetooth is not enabled, update notification manually."); 382 mNotifier.updateNotification(); 383 } 384 } 385 } 386