1 /* 2 * Copyright (C) 2011 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.contacts.calllog; 18 19 import com.android.common.io.MoreCloseables; 20 import com.android.contacts.ContactsUtils; 21 import com.android.contacts.R; 22 import com.android.contacts.activities.DialtactsActivity.ViewPagerVisibilityListener; 23 import com.android.contacts.util.EmptyLoader; 24 import com.android.contacts.voicemail.VoicemailStatusHelper; 25 import com.android.contacts.voicemail.VoicemailStatusHelper.StatusMessage; 26 import com.android.contacts.voicemail.VoicemailStatusHelperImpl; 27 import com.android.internal.telephony.CallerInfo; 28 import com.android.internal.telephony.ITelephony; 29 import com.google.common.annotations.VisibleForTesting; 30 31 import android.app.Activity; 32 import android.app.KeyguardManager; 33 import android.app.ListFragment; 34 import android.content.Context; 35 import android.content.Intent; 36 import android.database.Cursor; 37 import android.net.Uri; 38 import android.os.Bundle; 39 import android.os.RemoteException; 40 import android.os.ServiceManager; 41 import android.provider.CallLog.Calls; 42 import android.telephony.PhoneNumberUtils; 43 import android.text.TextUtils; 44 import android.util.Log; 45 import android.view.LayoutInflater; 46 import android.view.Menu; 47 import android.view.MenuInflater; 48 import android.view.MenuItem; 49 import android.view.View; 50 import android.view.ViewGroup; 51 import android.widget.ListView; 52 import android.widget.TextView; 53 54 import java.util.List; 55 56 /** 57 * Displays a list of call log entries. 58 */ 59 public class CallLogFragment extends ListFragment implements ViewPagerVisibilityListener, 60 CallLogQueryHandler.Listener, CallLogAdapter.CallFetcher { 61 private static final String TAG = "CallLogFragment"; 62 63 /** 64 * ID of the empty loader to defer other fragments. 65 */ 66 private static final int EMPTY_LOADER_ID = 0; 67 68 private CallLogAdapter mAdapter; 69 private CallLogQueryHandler mCallLogQueryHandler; 70 private boolean mScrollToTop; 71 72 private boolean mShowOptionsMenu; 73 /** Whether there is at least one voicemail source installed. */ 74 private boolean mVoicemailSourcesAvailable = false; 75 /** Whether we are currently filtering over voicemail. */ 76 private boolean mShowingVoicemailOnly = false; 77 78 private VoicemailStatusHelper mVoicemailStatusHelper; 79 private View mStatusMessageView; 80 private TextView mStatusMessageText; 81 private TextView mStatusMessageAction; 82 private KeyguardManager mKeyguardManager; 83 84 private boolean mEmptyLoaderRunning; 85 private boolean mCallLogFetched; 86 private boolean mVoicemailStatusFetched; 87 88 @Override 89 public void onCreate(Bundle state) { 90 super.onCreate(state); 91 92 mCallLogQueryHandler = new CallLogQueryHandler(getActivity().getContentResolver(), this); 93 mKeyguardManager = 94 (KeyguardManager) getActivity().getSystemService(Context.KEYGUARD_SERVICE); 95 setHasOptionsMenu(true); 96 } 97 98 /** Called by the CallLogQueryHandler when the list of calls has been fetched or updated. */ 99 @Override 100 public void onCallsFetched(Cursor cursor) { 101 if (getActivity() == null || getActivity().isFinishing()) { 102 return; 103 } 104 mAdapter.setLoading(false); 105 mAdapter.changeCursor(cursor); 106 // This will update the state of the "Clear call log" menu item. 107 getActivity().invalidateOptionsMenu(); 108 if (mScrollToTop) { 109 final ListView listView = getListView(); 110 if (listView.getFirstVisiblePosition() > 5) { 111 listView.setSelection(5); 112 } 113 listView.smoothScrollToPosition(0); 114 mScrollToTop = false; 115 } 116 mCallLogFetched = true; 117 destroyEmptyLoaderIfAllDataFetched(); 118 } 119 120 /** 121 * Called by {@link CallLogQueryHandler} after a successful query to voicemail status provider. 122 */ 123 @Override 124 public void onVoicemailStatusFetched(Cursor statusCursor) { 125 if (getActivity() == null || getActivity().isFinishing()) { 126 return; 127 } 128 updateVoicemailStatusMessage(statusCursor); 129 130 int activeSources = mVoicemailStatusHelper.getNumberActivityVoicemailSources(statusCursor); 131 setVoicemailSourcesAvailable(activeSources != 0); 132 MoreCloseables.closeQuietly(statusCursor); 133 mVoicemailStatusFetched = true; 134 destroyEmptyLoaderIfAllDataFetched(); 135 } 136 137 private void destroyEmptyLoaderIfAllDataFetched() { 138 if (mCallLogFetched && mVoicemailStatusFetched && mEmptyLoaderRunning) { 139 mEmptyLoaderRunning = false; 140 getLoaderManager().destroyLoader(EMPTY_LOADER_ID); 141 } 142 } 143 144 /** Sets whether there are any voicemail sources available in the platform. */ 145 private void setVoicemailSourcesAvailable(boolean voicemailSourcesAvailable) { 146 if (mVoicemailSourcesAvailable == voicemailSourcesAvailable) return; 147 mVoicemailSourcesAvailable = voicemailSourcesAvailable; 148 149 Activity activity = getActivity(); 150 if (activity != null) { 151 // This is so that the options menu content is updated. 152 activity.invalidateOptionsMenu(); 153 } 154 } 155 156 @Override 157 public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) { 158 View view = inflater.inflate(R.layout.call_log_fragment, container, false); 159 mVoicemailStatusHelper = new VoicemailStatusHelperImpl(); 160 mStatusMessageView = view.findViewById(R.id.voicemail_status); 161 mStatusMessageText = (TextView) view.findViewById(R.id.voicemail_status_message); 162 mStatusMessageAction = (TextView) view.findViewById(R.id.voicemail_status_action); 163 return view; 164 } 165 166 @Override 167 public void onViewCreated(View view, Bundle savedInstanceState) { 168 super.onViewCreated(view, savedInstanceState); 169 String currentCountryIso = ContactsUtils.getCurrentCountryIso(getActivity()); 170 mAdapter = new CallLogAdapter(getActivity(), this, 171 new ContactInfoHelper(getActivity(), currentCountryIso)); 172 setListAdapter(mAdapter); 173 getListView().setItemsCanFocus(true); 174 } 175 176 @Override 177 public void onStart() { 178 mScrollToTop = true; 179 180 // Start the empty loader now to defer other fragments. We destroy it when both calllog 181 // and the voicemail status are fetched. 182 getLoaderManager().initLoader(EMPTY_LOADER_ID, null, 183 new EmptyLoader.Callback(getActivity())); 184 mEmptyLoaderRunning = true; 185 super.onStart(); 186 } 187 188 @Override 189 public void onResume() { 190 super.onResume(); 191 refreshData(); 192 } 193 194 private void updateVoicemailStatusMessage(Cursor statusCursor) { 195 List<StatusMessage> messages = mVoicemailStatusHelper.getStatusMessages(statusCursor); 196 if (messages.size() == 0) { 197 mStatusMessageView.setVisibility(View.GONE); 198 } else { 199 mStatusMessageView.setVisibility(View.VISIBLE); 200 // TODO: Change the code to show all messages. For now just pick the first message. 201 final StatusMessage message = messages.get(0); 202 if (message.showInCallLog()) { 203 mStatusMessageText.setText(message.callLogMessageId); 204 } 205 if (message.actionMessageId != -1) { 206 mStatusMessageAction.setText(message.actionMessageId); 207 } 208 if (message.actionUri != null) { 209 mStatusMessageAction.setVisibility(View.VISIBLE); 210 mStatusMessageAction.setOnClickListener(new View.OnClickListener() { 211 @Override 212 public void onClick(View v) { 213 getActivity().startActivity( 214 new Intent(Intent.ACTION_VIEW, message.actionUri)); 215 } 216 }); 217 } else { 218 mStatusMessageAction.setVisibility(View.GONE); 219 } 220 } 221 } 222 223 @Override 224 public void onPause() { 225 super.onPause(); 226 // Kill the requests thread 227 mAdapter.stopRequestProcessing(); 228 } 229 230 @Override 231 public void onStop() { 232 super.onStop(); 233 updateOnExit(); 234 } 235 236 @Override 237 public void onDestroy() { 238 super.onDestroy(); 239 mAdapter.stopRequestProcessing(); 240 mAdapter.changeCursor(null); 241 } 242 243 @Override 244 public void fetchCalls() { 245 if (mShowingVoicemailOnly) { 246 mCallLogQueryHandler.fetchVoicemailOnly(); 247 } else { 248 mCallLogQueryHandler.fetchAllCalls(); 249 } 250 } 251 252 public void startCallsQuery() { 253 mAdapter.setLoading(true); 254 mCallLogQueryHandler.fetchAllCalls(); 255 if (mShowingVoicemailOnly) { 256 mShowingVoicemailOnly = false; 257 getActivity().invalidateOptionsMenu(); 258 } 259 } 260 261 private void startVoicemailStatusQuery() { 262 mCallLogQueryHandler.fetchVoicemailStatus(); 263 } 264 265 @Override 266 public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { 267 super.onCreateOptionsMenu(menu, inflater); 268 if (mShowOptionsMenu) { 269 inflater.inflate(R.menu.call_log_options, menu); 270 } 271 } 272 273 @Override 274 public void onPrepareOptionsMenu(Menu menu) { 275 if (mShowOptionsMenu) { 276 final MenuItem itemDeleteAll = menu.findItem(R.id.delete_all); 277 // Check if all the menu items are inflated correctly. As a shortcut, we assume all 278 // menu items are ready if the first item is non-null. 279 if (itemDeleteAll != null) { 280 itemDeleteAll.setEnabled(mAdapter != null && !mAdapter.isEmpty()); 281 menu.findItem(R.id.show_voicemails_only).setVisible( 282 mVoicemailSourcesAvailable && !mShowingVoicemailOnly); 283 menu.findItem(R.id.show_all_calls).setVisible( 284 mVoicemailSourcesAvailable && mShowingVoicemailOnly); 285 } 286 } 287 } 288 289 @Override 290 public boolean onOptionsItemSelected(MenuItem item) { 291 switch (item.getItemId()) { 292 case R.id.delete_all: 293 ClearCallLogDialog.show(getFragmentManager()); 294 return true; 295 296 case R.id.show_voicemails_only: 297 mCallLogQueryHandler.fetchVoicemailOnly(); 298 mShowingVoicemailOnly = true; 299 return true; 300 301 case R.id.show_all_calls: 302 mCallLogQueryHandler.fetchAllCalls(); 303 mShowingVoicemailOnly = false; 304 return true; 305 306 default: 307 return false; 308 } 309 } 310 311 public void callSelectedEntry() { 312 int position = getListView().getSelectedItemPosition(); 313 if (position < 0) { 314 // In touch mode you may often not have something selected, so 315 // just call the first entry to make sure that [send] [send] calls the 316 // most recent entry. 317 position = 0; 318 } 319 final Cursor cursor = (Cursor)mAdapter.getItem(position); 320 if (cursor != null) { 321 String number = cursor.getString(CallLogQuery.NUMBER); 322 if (TextUtils.isEmpty(number) 323 || number.equals(CallerInfo.UNKNOWN_NUMBER) 324 || number.equals(CallerInfo.PRIVATE_NUMBER) 325 || number.equals(CallerInfo.PAYPHONE_NUMBER)) { 326 // This number can't be called, do nothing 327 return; 328 } 329 Intent intent; 330 // If "number" is really a SIP address, construct a sip: URI. 331 if (PhoneNumberUtils.isUriNumber(number)) { 332 intent = new Intent(Intent.ACTION_CALL_PRIVILEGED, 333 Uri.fromParts("sip", number, null)); 334 } else { 335 // We're calling a regular PSTN phone number. 336 // Construct a tel: URI, but do some other possible cleanup first. 337 int callType = cursor.getInt(CallLogQuery.CALL_TYPE); 338 if (!number.startsWith("+") && 339 (callType == Calls.INCOMING_TYPE 340 || callType == Calls.MISSED_TYPE)) { 341 // If the caller-id matches a contact with a better qualified number, use it 342 String countryIso = cursor.getString(CallLogQuery.COUNTRY_ISO); 343 number = mAdapter.getBetterNumberFromContacts(number, countryIso); 344 } 345 intent = new Intent(Intent.ACTION_CALL_PRIVILEGED, 346 Uri.fromParts("tel", number, null)); 347 } 348 intent.setFlags( 349 Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS); 350 startActivity(intent); 351 } 352 } 353 354 @VisibleForTesting 355 CallLogAdapter getAdapter() { 356 return mAdapter; 357 } 358 359 @Override 360 public void onVisibilityChanged(boolean visible) { 361 if (mShowOptionsMenu != visible) { 362 mShowOptionsMenu = visible; 363 // Invalidate the options menu since we are changing the list of options shown in it. 364 Activity activity = getActivity(); 365 if (activity != null) { 366 activity.invalidateOptionsMenu(); 367 } 368 } 369 370 if (visible && isResumed()) { 371 refreshData(); 372 } 373 374 if (!visible) { 375 updateOnExit(); 376 } 377 } 378 379 /** Requests updates to the data to be shown. */ 380 private void refreshData() { 381 // Mark all entries in the contact info cache as out of date, so they will be looked up 382 // again once being shown. 383 mAdapter.invalidateCache(); 384 startCallsQuery(); 385 startVoicemailStatusQuery(); 386 updateOnEntry(); 387 } 388 389 /** Removes the missed call notifications. */ 390 private void removeMissedCallNotifications() { 391 try { 392 ITelephony telephony = 393 ITelephony.Stub.asInterface(ServiceManager.getService("phone")); 394 if (telephony != null) { 395 telephony.cancelMissedCallsNotification(); 396 } else { 397 Log.w(TAG, "Telephony service is null, can't call " + 398 "cancelMissedCallsNotification"); 399 } 400 } catch (RemoteException e) { 401 Log.e(TAG, "Failed to clear missed calls notification due to remote exception"); 402 } 403 } 404 405 /** Updates call data and notification state while leaving the call log tab. */ 406 private void updateOnExit() { 407 updateOnTransition(false); 408 } 409 410 /** Updates call data and notification state while entering the call log tab. */ 411 private void updateOnEntry() { 412 updateOnTransition(true); 413 } 414 415 private void updateOnTransition(boolean onEntry) { 416 // We don't want to update any call data when keyguard is on because the user has likely not 417 // seen the new calls yet. 418 if (!mKeyguardManager.inKeyguardRestrictedInputMode()) { 419 // On either of the transitions we reset the new flag and update the notifications. 420 // While exiting we additionally consume all missed calls (by marking them as read). 421 // This will ensure that they no more appear in the "new" section when we return back. 422 mCallLogQueryHandler.markNewCallsAsOld(); 423 if (!onEntry) { 424 mCallLogQueryHandler.markMissedCallsAsRead(); 425 } 426 removeMissedCallNotifications(); 427 updateVoicemailNotifications(); 428 } 429 } 430 431 private void updateVoicemailNotifications() { 432 Intent serviceIntent = new Intent(getActivity(), CallLogNotificationsService.class); 433 serviceIntent.setAction(CallLogNotificationsService.ACTION_UPDATE_NOTIFICATIONS); 434 getActivity().startService(serviceIntent); 435 } 436 } 437