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