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