• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 {
CustomContentObserver()88         public CustomContentObserver() {
89             super(mHandler);
90         }
91         @Override
onChange(boolean selfChange)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 
CallLogFragment()112     public CallLogFragment() {
113         this(CallLogQueryHandler.CALL_TYPE_ALL, -1);
114     }
115 
CallLogFragment(int filterType)116     public CallLogFragment(int filterType) {
117         this(filterType, -1);
118     }
119 
CallLogFragment(int filterType, int logLimit)120     public CallLogFragment(int filterType, int logLimit) {
121         super();
122         mCallTypeFilter = filterType;
123         mLogLimit = logLimit;
124     }
125 
126     @Override
onCreate(Bundle state)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
onCallsFetched(Cursor cursor)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
onVoicemailStatusFetched(Cursor statusCursor)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 
destroyEmptyLoaderIfAllDataFetched()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. */
setVoicemailSourcesAvailable(boolean voicemailSourcesAvailable)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
onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState)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
onViewCreated(View view, Bundle savedInstanceState)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), true, 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      */
configureScreenFromIntent(Intent newIntent)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
onStart()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
onResume()263     public void onResume() {
264         super.onResume();
265         refreshData();
266     }
267 
updateVoicemailStatusMessage(Cursor statusCursor)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
onPause()298     public void onPause() {
299         super.onPause();
300         // Kill the requests thread
301         mAdapter.stopRequestProcessing();
302     }
303 
304     @Override
onStop()305     public void onStop() {
306         super.onStop();
307         updateOnExit();
308     }
309 
310     @Override
onDestroy()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
fetchCalls()320     public void fetchCalls() {
321         mCallLogQueryHandler.fetchCalls(mCallTypeFilter);
322     }
323 
startCallsQuery()324     public void startCallsQuery() {
325         mAdapter.setLoading(true);
326         mCallLogQueryHandler.fetchCalls(mCallTypeFilter);
327     }
328 
startVoicemailStatusQuery()329     private void startVoicemailStatusQuery() {
330         mCallLogQueryHandler.fetchVoicemailStatus();
331     }
332 
updateCallList(int filterType)333     private void updateCallList(int filterType) {
334         mCallLogQueryHandler.fetchCalls(filterType);
335     }
336 
updateEmptyMessage(int filterType)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 
callSelectedEntry()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 
getAdapter()394     CallLogAdapter getAdapter() {
395         return mAdapter;
396     }
397 
398     @Override
setMenuVisibility(boolean menuVisible)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. */
refreshData()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. */
updateOnExit()426     private void updateOnExit() {
427         updateOnTransition(false);
428     }
429 
430     /** Updates call data and notification state while entering the call log tab. */
updateOnEntry()431     private void updateOnEntry() {
432         updateOnTransition(true);
433     }
434 
435     // TODO: Move to CallLogActivity
updateOnTransition(boolean onEntry)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