• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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