1 /* 2 * Copyright (C) 2017 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 package com.android.dialer.calllog.ui; 17 18 import android.database.Cursor; 19 import android.os.Bundle; 20 import android.support.annotation.Nullable; 21 import android.support.annotation.VisibleForTesting; 22 import android.support.v4.app.Fragment; 23 import android.support.v4.app.LoaderManager.LoaderCallbacks; 24 import android.support.v4.content.Loader; 25 import android.support.v4.content.LocalBroadcastManager; 26 import android.support.v7.widget.LinearLayoutManager; 27 import android.support.v7.widget.RecyclerView; 28 import android.view.LayoutInflater; 29 import android.view.View; 30 import android.view.ViewGroup; 31 import com.android.dialer.calllog.CallLogComponent; 32 import com.android.dialer.calllog.RefreshAnnotatedCallLogReceiver; 33 import com.android.dialer.common.LogUtil; 34 import com.android.dialer.common.concurrent.DefaultFutureCallback; 35 import com.android.dialer.common.concurrent.ThreadUtil; 36 import com.android.dialer.metrics.Metrics; 37 import com.android.dialer.metrics.MetricsComponent; 38 import com.android.dialer.metrics.jank.RecyclerViewJankLogger; 39 import com.google.common.util.concurrent.Futures; 40 import com.google.common.util.concurrent.MoreExecutors; 41 import java.util.concurrent.TimeUnit; 42 43 /** The "new" call log fragment implementation, which is built on top of the annotated call log. */ 44 public final class NewCallLogFragment extends Fragment implements LoaderCallbacks<Cursor> { 45 46 @VisibleForTesting 47 static final long MARK_ALL_CALLS_READ_WAIT_MILLIS = TimeUnit.SECONDS.toMillis(3); 48 49 private RefreshAnnotatedCallLogReceiver refreshAnnotatedCallLogReceiver; 50 private RecyclerView recyclerView; 51 52 private boolean shouldMarkCallsRead = false; 53 private final Runnable setShouldMarkCallsReadTrue = () -> shouldMarkCallsRead = true; 54 55 public NewCallLogFragment() { 56 LogUtil.enterBlock("NewCallLogFragment.NewCallLogFragment"); 57 } 58 59 @Override 60 public void onActivityCreated(@Nullable Bundle savedInstanceState) { 61 super.onActivityCreated(savedInstanceState); 62 63 LogUtil.enterBlock("NewCallLogFragment.onActivityCreated"); 64 65 refreshAnnotatedCallLogReceiver = new RefreshAnnotatedCallLogReceiver(getContext()); 66 } 67 68 @Override 69 public void onStart() { 70 super.onStart(); 71 72 LogUtil.enterBlock("NewCallLogFragment.onStart"); 73 } 74 75 @Override 76 public void onResume() { 77 super.onResume(); 78 79 boolean isHidden = isHidden(); 80 LogUtil.i("NewCallLogFragment.onResume", "isHidden = %s", isHidden); 81 82 // As a fragment's onResume() is tied to the containing Activity's onResume(), being resumed is 83 // not equivalent to becoming visible. 84 // For example, when an activity with a hidden fragment is resumed, the fragment's onResume() 85 // will be called but it is not visible. 86 if (!isHidden) { 87 onFragmentShown(); 88 } 89 } 90 91 @Override 92 public void onPause() { 93 super.onPause(); 94 LogUtil.enterBlock("NewCallLogFragment.onPause"); 95 96 onFragmentHidden(); 97 } 98 99 @Override 100 public void onHiddenChanged(boolean hidden) { 101 super.onHiddenChanged(hidden); 102 LogUtil.i("NewCallLogFragment.onHiddenChanged", "hidden = %s", hidden); 103 104 if (hidden) { 105 onFragmentHidden(); 106 } else { 107 onFragmentShown(); 108 } 109 } 110 111 /** 112 * To be called when the fragment becomes visible. 113 * 114 * <p>Note that for a fragment, being resumed is not equivalent to becoming visible. 115 * 116 * <p>For example, when an activity with a hidden fragment is resumed, the fragment's onResume() 117 * will be called but it is not visible. 118 */ 119 private void onFragmentShown() { 120 registerRefreshAnnotatedCallLogReceiver(); 121 122 CallLogComponent.get(getContext()) 123 .getRefreshAnnotatedCallLogNotifier() 124 .notify(/* checkDirty = */ true); 125 126 // There are some types of data that we show in the call log that are not represented in the 127 // AnnotatedCallLog. For example, CP2 information for invalid numbers can sometimes only be 128 // fetched at display time. Because of this, we need to clear the adapter's cache and update it 129 // whenever the user arrives at the call log (rather than relying on changes to the CursorLoader 130 // alone). 131 if (recyclerView.getAdapter() != null) { 132 ((NewCallLogAdapter) recyclerView.getAdapter()).clearCache(); 133 recyclerView.getAdapter().notifyDataSetChanged(); 134 } 135 136 // We shouldn't mark the calls as read immediately when the 3 second timer expires because we 137 // don't want to disrupt the UI; instead we set a bit indicating to mark them read when the user 138 // leaves the fragment (in onPause). 139 shouldMarkCallsRead = false; 140 ThreadUtil.getUiThreadHandler() 141 .postDelayed(setShouldMarkCallsReadTrue, MARK_ALL_CALLS_READ_WAIT_MILLIS); 142 } 143 144 /** 145 * To be called when the fragment becomes hidden. 146 * 147 * <p>This can happen in the following two cases: 148 * 149 * <ul> 150 * <li>hide the fragment but keep the parent activity visible (e.g., calling {@link 151 * android.support.v4.app.FragmentTransaction#hide(Fragment)} in an activity, or 152 * <li>the parent activity is paused. 153 * </ul> 154 */ 155 private void onFragmentHidden() { 156 // This is pending work that we don't actually need to follow through with. 157 ThreadUtil.getUiThreadHandler().removeCallbacks(setShouldMarkCallsReadTrue); 158 159 unregisterRefreshAnnotatedCallLogReceiver(); 160 161 if (shouldMarkCallsRead) { 162 Futures.addCallback( 163 CallLogComponent.get(getContext()).getClearMissedCalls().clearAll(), 164 new DefaultFutureCallback<>(), 165 MoreExecutors.directExecutor()); 166 } 167 } 168 169 @Override 170 public View onCreateView( 171 LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { 172 LogUtil.enterBlock("NewCallLogFragment.onCreateView"); 173 174 View view = inflater.inflate(R.layout.new_call_log_fragment, container, false); 175 recyclerView = view.findViewById(R.id.new_call_log_recycler_view); 176 recyclerView.addOnScrollListener( 177 new RecyclerViewJankLogger( 178 MetricsComponent.get(getContext()).metrics(), Metrics.NEW_CALL_LOG_JANK_EVENT_NAME)); 179 180 getLoaderManager().restartLoader(0, null, this); 181 182 return view; 183 } 184 185 private void registerRefreshAnnotatedCallLogReceiver() { 186 LogUtil.enterBlock("NewCallLogFragment.registerRefreshAnnotatedCallLogReceiver"); 187 188 LocalBroadcastManager.getInstance(getContext()) 189 .registerReceiver( 190 refreshAnnotatedCallLogReceiver, RefreshAnnotatedCallLogReceiver.getIntentFilter()); 191 } 192 193 private void unregisterRefreshAnnotatedCallLogReceiver() { 194 LogUtil.enterBlock("NewCallLogFragment.unregisterRefreshAnnotatedCallLogReceiver"); 195 196 // Cancel pending work as we don't need it any more. 197 CallLogComponent.get(getContext()).getRefreshAnnotatedCallLogNotifier().cancel(); 198 199 LocalBroadcastManager.getInstance(getContext()) 200 .unregisterReceiver(refreshAnnotatedCallLogReceiver); 201 } 202 203 @Override 204 public Loader<Cursor> onCreateLoader(int id, Bundle args) { 205 LogUtil.enterBlock("NewCallLogFragment.onCreateLoader"); 206 return new CoalescedAnnotatedCallLogCursorLoader(getContext()); 207 } 208 209 @Override 210 public void onLoadFinished(Loader<Cursor> loader, Cursor newCursor) { 211 LogUtil.enterBlock("NewCallLogFragment.onLoadFinished"); 212 213 if (newCursor == null) { 214 // This might be possible when the annotated call log hasn't been created but we're trying 215 // to show the call log. 216 LogUtil.w("NewCallLogFragment.onLoadFinished", "null cursor"); 217 return; 218 } 219 220 // TODO(zachh): Handle empty cursor by showing empty view. 221 if (recyclerView.getAdapter() == null) { 222 recyclerView.setLayoutManager(new LinearLayoutManager(getContext())); 223 recyclerView.setAdapter( 224 new NewCallLogAdapter(getContext(), newCursor, System::currentTimeMillis)); 225 } else { 226 ((NewCallLogAdapter) recyclerView.getAdapter()).updateCursor(newCursor); 227 } 228 } 229 230 @Override 231 public void onLoaderReset(Loader<Cursor> loader) { 232 LogUtil.enterBlock("NewCallLogFragment.onLoaderReset"); 233 recyclerView.setAdapter(null); 234 } 235 } 236