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 com.android.bluetooth.BluetoothMethodProxy; 58 import com.android.bluetooth.BluetoothStatsLog; 59 import com.android.bluetooth.R; 60 import com.android.bluetooth.content_profiles.ContentProfileErrorReportUtils; 61 import com.android.bluetooth.flags.Flags; 62 63 /** 64 * View showing the user's finished bluetooth opp transfers that the user does not confirm. 65 * Including outbound and inbound transfers, both successful and failed. 66 */ 67 // Next tag value for ContentProfileErrorReportUtils.report(): 2 68 public class BluetoothOppTransferHistory extends Activity 69 implements View.OnCreateContextMenuListener, OnItemClickListener { 70 private static final String TAG = "BluetoothOppTransferHistory"; 71 72 private ListView mListView; 73 74 private Cursor mTransferCursor; 75 76 private BluetoothOppTransferAdapter mTransferAdapter; 77 78 private int mIdColumnId; 79 80 private int mContextMenuPosition; 81 82 private boolean mContextMenu = false; 83 84 /** Class to handle Notification Manager updates */ 85 private BluetoothOppNotification mNotifier; 86 87 @Override onCreate(Bundle icicle)88 public void onCreate(Bundle icicle) { 89 super.onCreate(icicle); 90 91 // TODO(b/309578419): Make this activity handle insets properly and then remove this. 92 getTheme().applyStyle(R.style.OptOutEdgeToEdgeEnforcement, /* force */ false); 93 94 setContentView(R.layout.bluetooth_transfers_page); 95 mListView = (ListView) findViewById(R.id.list); 96 mListView.setEmptyView(findViewById(R.id.empty)); 97 98 String direction; 99 100 boolean isOutbound = false; 101 102 if (Flags.oppStartActivityDirectlyFromNotification()) { 103 String action = getIntent().getAction(); 104 isOutbound = Constants.ACTION_OPEN_OUTBOUND_TRANSFER.equals(action); 105 } else { 106 int dir = getIntent().getIntExtra(Constants.EXTRA_DIRECTION, 0); 107 isOutbound = (dir == BluetoothShare.DIRECTION_OUTBOUND); 108 } 109 110 if (isOutbound) { 111 setTitle(getText(R.string.outbound_history_title)); 112 direction = 113 "(" 114 + BluetoothShare.DIRECTION 115 + " == " 116 + BluetoothShare.DIRECTION_OUTBOUND 117 + ")"; 118 } else { 119 setTitle(getText(R.string.inbound_history_title)); 120 direction = 121 "(" 122 + BluetoothShare.DIRECTION 123 + " == " 124 + BluetoothShare.DIRECTION_INBOUND 125 + ")"; 126 } 127 128 String selection = 129 BluetoothShare.STATUS 130 + " >= '200' AND " 131 + direction 132 + " AND (" 133 + BluetoothShare.VISIBILITY 134 + " IS NULL OR " 135 + BluetoothShare.VISIBILITY 136 + " == '" 137 + BluetoothShare.VISIBILITY_VISIBLE 138 + "')"; 139 140 final String sortOrder = BluetoothShare.TIMESTAMP + " DESC"; 141 mTransferCursor = 142 BluetoothMethodProxy.getInstance() 143 .contentResolverQuery( 144 getContentResolver(), 145 BluetoothShare.CONTENT_URI, 146 new String[] { 147 "_id", 148 BluetoothShare.FILENAME_HINT, 149 BluetoothShare.STATUS, 150 BluetoothShare.TOTAL_BYTES, 151 BluetoothShare._DATA, 152 BluetoothShare.TIMESTAMP, 153 BluetoothShare.VISIBILITY, 154 BluetoothShare.DESTINATION, 155 BluetoothShare.DIRECTION 156 }, 157 selection, 158 null, 159 sortOrder); 160 161 // only attach everything to the listbox if we can access 162 // the transfer database. Otherwise, just show it empty 163 if (mTransferCursor != null) { 164 mIdColumnId = mTransferCursor.getColumnIndexOrThrow(BluetoothShare._ID); 165 // Create a list "controller" for the data 166 mTransferAdapter = 167 new BluetoothOppTransferAdapter( 168 this, R.layout.bluetooth_transfer_item, mTransferCursor); 169 mListView.setAdapter(mTransferAdapter); 170 mListView.setScrollBarStyle(View.SCROLLBARS_INSIDE_INSET); 171 mListView.setOnCreateContextMenuListener(this); 172 mListView.setOnItemClickListener(this); 173 } 174 175 mNotifier = new BluetoothOppNotification(this); 176 mContextMenu = false; 177 } 178 179 @Override onCreateOptionsMenu(Menu menu)180 public boolean onCreateOptionsMenu(Menu menu) { 181 if (mTransferCursor != null) { 182 MenuInflater inflater = getMenuInflater(); 183 inflater.inflate(R.menu.transferhistory, menu); 184 } 185 return true; 186 } 187 188 @Override onPrepareOptionsMenu(Menu menu)189 public boolean onPrepareOptionsMenu(Menu menu) { 190 menu.findItem(R.id.transfer_menu_clear_all).setEnabled(isTransferComplete()); 191 return super.onPrepareOptionsMenu(menu); 192 } 193 194 @Override onOptionsItemSelected(MenuItem item)195 public boolean onOptionsItemSelected(MenuItem item) { 196 switch (item.getItemId()) { 197 case R.id.transfer_menu_clear_all: 198 promptClearList(); 199 return true; 200 } 201 return false; 202 } 203 204 @Override onContextItemSelected(MenuItem item)205 public boolean onContextItemSelected(MenuItem item) { 206 if (mTransferCursor.getCount() == 0) { 207 Log.i(TAG, "History is already cleared, not clearing again"); 208 return true; 209 } 210 mTransferCursor.moveToPosition(mContextMenuPosition); 211 switch (item.getItemId()) { 212 case R.id.transfer_menu_open: 213 openCompleteTransfer(); 214 updateNotificationWhenBtDisabled(); 215 return true; 216 217 case R.id.transfer_menu_clear: 218 int sessionId = mTransferCursor.getInt(mIdColumnId); 219 Uri contentUri = Uri.parse(BluetoothShare.CONTENT_URI + "/" + sessionId); 220 BluetoothOppUtility.updateVisibilityToHidden(this, contentUri); 221 updateNotificationWhenBtDisabled(); 222 return true; 223 } 224 return false; 225 } 226 227 @Override onDestroy()228 protected void onDestroy() { 229 if (mTransferCursor != null) { 230 mTransferCursor.close(); 231 } 232 super.onDestroy(); 233 } 234 235 @Override onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo)236 public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { 237 if (mTransferCursor != null) { 238 mContextMenu = true; 239 AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuInfo; 240 mTransferCursor.moveToPosition(info.position); 241 mContextMenuPosition = info.position; 242 243 String fileName = 244 mTransferCursor.getString( 245 mTransferCursor.getColumnIndexOrThrow(BluetoothShare.FILENAME_HINT)); 246 if (fileName == null) { 247 fileName = this.getString(R.string.unknown_file); 248 } 249 menu.setHeaderTitle(fileName); 250 getMenuInflater().inflate(R.menu.transferhistorycontextfinished, menu); 251 } 252 } 253 254 /** Prompt the user if they would like to clear the transfer history */ promptClearList()255 private void promptClearList() { 256 new AlertDialog.Builder(this) 257 .setTitle(R.string.transfer_clear_dlg_title) 258 .setMessage(R.string.transfer_clear_dlg_msg) 259 .setPositiveButton( 260 android.R.string.ok, 261 new DialogInterface.OnClickListener() { 262 @Override 263 public void onClick(DialogInterface dialog, int whichButton) { 264 clearAllDownloads(); 265 } 266 }) 267 .setNegativeButton(android.R.string.cancel, null) 268 .show(); 269 } 270 271 /** Returns true if the device has finished transfers, including error and success. */ isTransferComplete()272 private boolean isTransferComplete() { 273 try { 274 if (mTransferCursor.moveToFirst()) { 275 while (!mTransferCursor.isAfterLast()) { 276 int statusColumnId = 277 mTransferCursor.getColumnIndexOrThrow(BluetoothShare.STATUS); 278 int status = mTransferCursor.getInt(statusColumnId); 279 if (BluetoothShare.isStatusCompleted(status)) { 280 return true; 281 } 282 mTransferCursor.moveToNext(); 283 } 284 } 285 } catch (StaleDataException e) { 286 ContentProfileErrorReportUtils.report( 287 BluetoothProfile.OPP, 288 BluetoothProtoEnums.BLUETOOTH_OPP_TRANSFER_HISTORY, 289 BluetoothStatsLog.BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__EXCEPTION, 290 0); 291 } 292 return false; 293 } 294 295 /** Clear all finished transfers, error and success transfer items. */ clearAllDownloads()296 private void clearAllDownloads() { 297 if (mTransferCursor.moveToFirst()) { 298 while (!mTransferCursor.isAfterLast()) { 299 int sessionId = mTransferCursor.getInt(mIdColumnId); 300 Uri contentUri = Uri.parse(BluetoothShare.CONTENT_URI + "/" + sessionId); 301 BluetoothOppUtility.updateVisibilityToHidden(this, contentUri); 302 303 mTransferCursor.moveToNext(); 304 } 305 updateNotificationWhenBtDisabled(); 306 } 307 } 308 309 /* 310 * (non-Javadoc) 311 * @see 312 * android.widget.AdapterView.OnItemClickListener#onItemClick(android.widget 313 * .AdapterView, android.view.View, int, long) 314 */ 315 @Override onItemClick(AdapterView<?> parent, View view, int position, long id)316 public void onItemClick(AdapterView<?> parent, View view, int position, long id) { 317 // Open the selected item 318 Log.v(TAG, "onItemClick: ContextMenu = " + mContextMenu); 319 if (!mContextMenu) { 320 mTransferCursor.moveToPosition(position); 321 openCompleteTransfer(); 322 updateNotificationWhenBtDisabled(); 323 } 324 mContextMenu = false; 325 } 326 327 /** 328 * Open the selected finished transfer. mDownloadCursor must be moved to appropriate position 329 * before calling this function 330 */ openCompleteTransfer()331 private void openCompleteTransfer() { 332 int sessionId = mTransferCursor.getInt(mIdColumnId); 333 Uri contentUri = Uri.parse(BluetoothShare.CONTENT_URI + "/" + sessionId); 334 BluetoothOppTransferInfo transInfo = BluetoothOppUtility.queryRecord(this, contentUri); 335 if (transInfo == null) { 336 Log.e(TAG, "Error: Can not get data from db"); 337 ContentProfileErrorReportUtils.report( 338 BluetoothProfile.OPP, 339 BluetoothProtoEnums.BLUETOOTH_OPP_TRANSFER_HISTORY, 340 BluetoothStatsLog.BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__LOG_ERROR, 341 1); 342 return; 343 } 344 if (transInfo.mDirection == BluetoothShare.DIRECTION_INBOUND 345 && BluetoothShare.isStatusSuccess(transInfo.mStatus)) { 346 // if received file successfully, open this file 347 BluetoothOppUtility.updateVisibilityToHidden(this, contentUri); 348 BluetoothOppUtility.openReceivedFile( 349 this, 350 transInfo.mFileName, 351 transInfo.mFileType, 352 transInfo.mTimeStamp, 353 contentUri); 354 } else { 355 Intent in = new Intent(this, BluetoothOppTransferActivity.class); 356 in.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 357 in.setDataAndNormalize(contentUri); 358 this.startActivity(in); 359 } 360 } 361 362 /** 363 * When Bluetooth is disabled, notification can not be updated by ContentObserver in OppService, 364 * so need update manually. 365 */ updateNotificationWhenBtDisabled()366 private void updateNotificationWhenBtDisabled() { 367 BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter(); 368 if (!adapter.isEnabled()) { 369 Log.v(TAG, "Bluetooth is not enabled, update notification manually."); 370 mNotifier.updateNotification(); 371 } 372 } 373 } 374