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