CallLogFragment.java revision 45ed3b5932ed590b45235d7b2befa736a95e7f75
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 17package com.android.dialer.calllog; 18 19import android.app.Activity; 20import android.app.KeyguardManager; 21import android.app.ListFragment; 22import android.content.Context; 23import android.content.Intent; 24import android.database.ContentObserver; 25import android.database.Cursor; 26import android.net.Uri; 27import android.os.Bundle; 28import android.os.Handler; 29import android.provider.CallLog; 30import android.provider.CallLog.Calls; 31import android.provider.ContactsContract; 32import android.provider.VoicemailContract.Status; 33import android.telephony.PhoneNumberUtils; 34import android.telephony.TelephonyManager; 35import android.view.LayoutInflater; 36import android.view.View; 37import android.view.ViewGroup; 38import android.widget.ListView; 39import android.widget.TextView; 40 41import com.android.common.io.MoreCloseables; 42import com.android.contacts.common.CallUtil; 43import com.android.contacts.common.GeoUtil; 44import com.android.contacts.common.util.PhoneNumberHelper; 45import com.android.dialer.R; 46import com.android.dialer.util.EmptyLoader; 47import com.android.dialer.voicemail.VoicemailStatusHelper; 48import com.android.dialer.voicemail.VoicemailStatusHelper.StatusMessage; 49import com.android.dialer.voicemail.VoicemailStatusHelperImpl; 50import com.android.dialerbind.ObjectFactory; 51import com.android.internal.telephony.ITelephony; 52 53import java.util.List; 54 55/** 56 * Displays a list of call log entries. To filter for a particular kind of call 57 * (all, missed or voicemails), specify it in the constructor. 58 */ 59public class CallLogFragment extends ListFragment 60 implements 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 /** Whether there is at least one voicemail source installed. */ 73 private boolean mVoicemailSourcesAvailable = false; 74 75 private VoicemailStatusHelper mVoicemailStatusHelper; 76 private View mStatusMessageView; 77 private TextView mStatusMessageText; 78 private TextView mStatusMessageAction; 79 private KeyguardManager mKeyguardManager; 80 81 private boolean mEmptyLoaderRunning; 82 private boolean mCallLogFetched; 83 private boolean mVoicemailStatusFetched; 84 85 private final Handler mHandler = new Handler(); 86 87 private TelephonyManager mTelephonyManager; 88 89 private class CustomContentObserver extends ContentObserver { 90 public CustomContentObserver() { 91 super(mHandler); 92 } 93 @Override 94 public void onChange(boolean selfChange) { 95 mRefreshDataRequired = true; 96 } 97 } 98 99 // See issue 6363009 100 private final ContentObserver mCallLogObserver = new CustomContentObserver(); 101 private final ContentObserver mContactsObserver = new CustomContentObserver(); 102 private final ContentObserver mVoicemailStatusObserver = new CustomContentObserver(); 103 private boolean mRefreshDataRequired = true; 104 105 // Exactly same variable is in Fragment as a package private. 106 private boolean mMenuVisible = true; 107 108 // Default to all calls. 109 private int mCallTypeFilter = CallLogQueryHandler.CALL_TYPE_ALL; 110 111 // Log limit - if no limit is specified, then the default in {@link CallLogQueryHandler} 112 // will be used. 113 private int mLogLimit = -1; 114 115 public CallLogFragment() { 116 this(CallLogQueryHandler.CALL_TYPE_ALL, -1); 117 } 118 119 public CallLogFragment(int filterType) { 120 this(filterType, -1); 121 } 122 123 public CallLogFragment(int filterType, int logLimit) { 124 super(); 125 mCallTypeFilter = filterType; 126 mLogLimit = logLimit; 127 } 128 129 @Override 130 public void onCreate(Bundle state) { 131 super.onCreate(state); 132 133 mCallLogQueryHandler = new CallLogQueryHandler(getActivity().getContentResolver(), 134 this, mLogLimit); 135 mKeyguardManager = 136 (KeyguardManager) getActivity().getSystemService(Context.KEYGUARD_SERVICE); 137 getActivity().getContentResolver().registerContentObserver(CallLog.CONTENT_URI, true, 138 mCallLogObserver); 139 getActivity().getContentResolver().registerContentObserver( 140 ContactsContract.Contacts.CONTENT_URI, true, mContactsObserver); 141 getActivity().getContentResolver().registerContentObserver( 142 Status.CONTENT_URI, true, mVoicemailStatusObserver); 143 setHasOptionsMenu(true); 144 updateCallList(mCallTypeFilter); 145 } 146 147 /** Called by the CallLogQueryHandler when the list of calls has been fetched or updated. */ 148 @Override 149 public void onCallsFetched(Cursor cursor) { 150 if (getActivity() == null || getActivity().isFinishing()) { 151 return; 152 } 153 mAdapter.setLoading(false); 154 mAdapter.changeCursor(cursor); 155 // This will update the state of the "Clear call log" menu item. 156 getActivity().invalidateOptionsMenu(); 157 if (mScrollToTop) { 158 final ListView listView = getListView(); 159 // The smooth-scroll animation happens over a fixed time period. 160 // As a result, if it scrolls through a large portion of the list, 161 // each frame will jump so far from the previous one that the user 162 // will not experience the illusion of downward motion. Instead, 163 // if we're not already near the top of the list, we instantly jump 164 // near the top, and animate from there. 165 if (listView.getFirstVisiblePosition() > 5) { 166 listView.setSelection(5); 167 } 168 // Workaround for framework issue: the smooth-scroll doesn't 169 // occur if setSelection() is called immediately before. 170 mHandler.post(new Runnable() { 171 @Override 172 public void run() { 173 if (getActivity() == null || getActivity().isFinishing()) { 174 return; 175 } 176 listView.smoothScrollToPosition(0); 177 } 178 }); 179 180 mScrollToTop = false; 181 } 182 mCallLogFetched = true; 183 destroyEmptyLoaderIfAllDataFetched(); 184 } 185 186 /** 187 * Called by {@link CallLogQueryHandler} after a successful query to voicemail status provider. 188 */ 189 @Override 190 public void onVoicemailStatusFetched(Cursor statusCursor) { 191 if (getActivity() == null || getActivity().isFinishing()) { 192 return; 193 } 194 updateVoicemailStatusMessage(statusCursor); 195 196 int activeSources = mVoicemailStatusHelper.getNumberActivityVoicemailSources(statusCursor); 197 setVoicemailSourcesAvailable(activeSources != 0); 198 MoreCloseables.closeQuietly(statusCursor); 199 mVoicemailStatusFetched = true; 200 destroyEmptyLoaderIfAllDataFetched(); 201 } 202 203 private void destroyEmptyLoaderIfAllDataFetched() { 204 if (mCallLogFetched && mVoicemailStatusFetched && mEmptyLoaderRunning) { 205 mEmptyLoaderRunning = false; 206 getLoaderManager().destroyLoader(EMPTY_LOADER_ID); 207 } 208 } 209 210 /** Sets whether there are any voicemail sources available in the platform. */ 211 private void setVoicemailSourcesAvailable(boolean voicemailSourcesAvailable) { 212 if (mVoicemailSourcesAvailable == voicemailSourcesAvailable) return; 213 mVoicemailSourcesAvailable = voicemailSourcesAvailable; 214 215 Activity activity = getActivity(); 216 if (activity != null) { 217 // This is so that the options menu content is updated. 218 activity.invalidateOptionsMenu(); 219 } 220 } 221 222 @Override 223 public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) { 224 View view = inflater.inflate(R.layout.call_log_fragment, container, false); 225 mVoicemailStatusHelper = new VoicemailStatusHelperImpl(); 226 mStatusMessageView = view.findViewById(R.id.voicemail_status); 227 mStatusMessageText = (TextView) view.findViewById(R.id.voicemail_status_message); 228 mStatusMessageAction = (TextView) view.findViewById(R.id.voicemail_status_action); 229 return view; 230 } 231 232 @Override 233 public void onViewCreated(View view, Bundle savedInstanceState) { 234 super.onViewCreated(view, savedInstanceState); 235 updateEmptyMessage(mCallTypeFilter); 236 String currentCountryIso = GeoUtil.getCurrentCountryIso(getActivity()); 237 mAdapter = ObjectFactory.newCallLogAdapter(getActivity(), this, new ContactInfoHelper( 238 getActivity(), currentCountryIso), true, true); 239 setListAdapter(mAdapter); 240 getListView().setItemsCanFocus(true); 241 } 242 243 /** 244 * Based on the new intent, decide whether the list should be configured 245 * to scroll up to display the first item. 246 */ 247 public void configureScreenFromIntent(Intent newIntent) { 248 // Typically, when switching to the call-log we want to show the user 249 // the same section of the list that they were most recently looking 250 // at. However, under some circumstances, we want to automatically 251 // scroll to the top of the list to present the newest call items. 252 // For example, immediately after a call is finished, we want to 253 // display information about that call. 254 mScrollToTop = Calls.CONTENT_TYPE.equals(newIntent.getType()); 255 } 256 257 @Override 258 public void onStart() { 259 // Start the empty loader now to defer other fragments. We destroy it when both calllog 260 // and the voicemail status are fetched. 261 getLoaderManager().initLoader(EMPTY_LOADER_ID, null, 262 new EmptyLoader.Callback(getActivity())); 263 mEmptyLoaderRunning = true; 264 super.onStart(); 265 } 266 267 @Override 268 public void onResume() { 269 super.onResume(); 270 refreshData(); 271 } 272 273 private void updateVoicemailStatusMessage(Cursor statusCursor) { 274 List<StatusMessage> messages = mVoicemailStatusHelper.getStatusMessages(statusCursor); 275 if (messages.size() == 0) { 276 mStatusMessageView.setVisibility(View.GONE); 277 } else { 278 mStatusMessageView.setVisibility(View.VISIBLE); 279 // TODO: Change the code to show all messages. For now just pick the first message. 280 final StatusMessage message = messages.get(0); 281 if (message.showInCallLog()) { 282 mStatusMessageText.setText(message.callLogMessageId); 283 } 284 if (message.actionMessageId != -1) { 285 mStatusMessageAction.setText(message.actionMessageId); 286 } 287 if (message.actionUri != null) { 288 mStatusMessageAction.setVisibility(View.VISIBLE); 289 mStatusMessageAction.setOnClickListener(new View.OnClickListener() { 290 @Override 291 public void onClick(View v) { 292 getActivity().startActivity( 293 new Intent(Intent.ACTION_VIEW, message.actionUri)); 294 } 295 }); 296 } else { 297 mStatusMessageAction.setVisibility(View.GONE); 298 } 299 } 300 } 301 302 @Override 303 public void onPause() { 304 super.onPause(); 305 // Kill the requests thread 306 mAdapter.stopRequestProcessing(); 307 } 308 309 @Override 310 public void onStop() { 311 super.onStop(); 312 updateOnExit(); 313 } 314 315 @Override 316 public void onDestroy() { 317 super.onDestroy(); 318 mAdapter.stopRequestProcessing(); 319 mAdapter.changeCursor(null); 320 getActivity().getContentResolver().unregisterContentObserver(mCallLogObserver); 321 getActivity().getContentResolver().unregisterContentObserver(mContactsObserver); 322 getActivity().getContentResolver().unregisterContentObserver(mVoicemailStatusObserver); 323 } 324 325 @Override 326 public void fetchCalls() { 327 mCallLogQueryHandler.fetchCalls(mCallTypeFilter); 328 } 329 330 public void startCallsQuery() { 331 mAdapter.setLoading(true); 332 mCallLogQueryHandler.fetchCalls(mCallTypeFilter); 333 } 334 335 private void startVoicemailStatusQuery() { 336 mCallLogQueryHandler.fetchVoicemailStatus(); 337 } 338 339 private void updateCallList(int filterType) { 340 mCallLogQueryHandler.fetchCalls(filterType); 341 } 342 343 private void updateEmptyMessage(int filterType) { 344 final String message; 345 switch (filterType) { 346 case Calls.MISSED_TYPE: 347 message = getString(R.string.recentMissed_empty); 348 break; 349 case CallLogQueryHandler.CALL_TYPE_ALL: 350 message = getString(R.string.recentCalls_empty); 351 break; 352 default: 353 throw new IllegalArgumentException("Unexpected filter type in CallLogFragment: " 354 + filterType); 355 } 356 ((TextView) getListView().getEmptyView()).setText(message); 357 } 358 359 public void callSelectedEntry() { 360 int position = getListView().getSelectedItemPosition(); 361 if (position < 0) { 362 // In touch mode you may often not have something selected, so 363 // just call the first entry to make sure that [send] [send] calls the 364 // most recent entry. 365 position = 0; 366 } 367 final Cursor cursor = (Cursor)mAdapter.getItem(position); 368 if (cursor != null) { 369 String number = cursor.getString(CallLogQuery.NUMBER); 370 int numberPresentation = cursor.getInt(CallLogQuery.NUMBER_PRESENTATION); 371 if (!PhoneNumberUtilsWrapper.canPlaceCallsTo(number, numberPresentation)) { 372 // This number can't be called, do nothing 373 return; 374 } 375 Intent intent; 376 // If "number" is really a SIP address, construct a sip: URI. 377 if (PhoneNumberHelper.isUriNumber(number)) { 378 intent = CallUtil.getCallIntent( 379 Uri.fromParts(CallUtil.SCHEME_SIP, number, null)); 380 } else { 381 // We're calling a regular PSTN phone number. 382 // Construct a tel: URI, but do some other possible cleanup first. 383 int callType = cursor.getInt(CallLogQuery.CALL_TYPE); 384 if (!number.startsWith("+") && 385 (callType == Calls.INCOMING_TYPE 386 || callType == Calls.MISSED_TYPE)) { 387 // If the caller-id matches a contact with a better qualified number, use it 388 String countryIso = cursor.getString(CallLogQuery.COUNTRY_ISO); 389 number = mAdapter.getBetterNumberFromContacts(number, countryIso); 390 } 391 intent = CallUtil.getCallIntent( 392 Uri.fromParts(CallUtil.SCHEME_TEL, number, null)); 393 } 394 intent.setFlags( 395 Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS); 396 startActivity(intent); 397 } 398 } 399 400 CallLogAdapter getAdapter() { 401 return mAdapter; 402 } 403 404 @Override 405 public void setMenuVisibility(boolean menuVisible) { 406 super.setMenuVisibility(menuVisible); 407 if (mMenuVisible != menuVisible) { 408 mMenuVisible = menuVisible; 409 if (!menuVisible) { 410 updateOnExit(); 411 } else if (isResumed()) { 412 refreshData(); 413 } 414 } 415 } 416 417 /** Requests updates to the data to be shown. */ 418 private void refreshData() { 419 // Prevent unnecessary refresh. 420 if (mRefreshDataRequired) { 421 // Mark all entries in the contact info cache as out of date, so they will be looked up 422 // again once being shown. 423 mAdapter.invalidateCache(); 424 startCallsQuery(); 425 startVoicemailStatusQuery(); 426 updateOnEntry(); 427 mRefreshDataRequired = false; 428 } 429 } 430 431 /** Updates call data and notification state while leaving the call log tab. */ 432 private void updateOnExit() { 433 updateOnTransition(false); 434 } 435 436 /** Updates call data and notification state while entering the call log tab. */ 437 private void updateOnEntry() { 438 updateOnTransition(true); 439 } 440 441 // TODO: Move to CallLogActivity 442 private void updateOnTransition(boolean onEntry) { 443 // We don't want to update any call data when keyguard is on because the user has likely not 444 // seen the new calls yet. 445 // This might be called before onCreate() and thus we need to check null explicitly. 446 if (mKeyguardManager != null && !mKeyguardManager.inKeyguardRestrictedInputMode()) { 447 // On either of the transitions we update the missed call and voicemail notifications. 448 // While exiting we additionally consume all missed calls (by marking them as read). 449 mCallLogQueryHandler.markNewCallsAsOld(); 450 if (!onEntry) { 451 mCallLogQueryHandler.markMissedCallsAsRead(); 452 } 453 CallLogNotificationsHelper.removeMissedCallNotifications(); 454 CallLogNotificationsHelper.updateVoicemailNotifications(getActivity()); 455 } 456 } 457} 458