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.app.Activity; 19 import android.database.Cursor; 20 import android.os.Bundle; 21 import android.support.annotation.Nullable; 22 import android.support.annotation.VisibleForTesting; 23 import android.support.v4.app.Fragment; 24 import android.support.v4.app.LoaderManager; 25 import android.support.v4.app.LoaderManager.LoaderCallbacks; 26 import android.support.v4.content.Loader; 27 import android.support.v4.content.LocalBroadcastManager; 28 import android.support.v7.widget.LinearLayoutManager; 29 import android.support.v7.widget.RecyclerView; 30 import android.view.LayoutInflater; 31 import android.view.View; 32 import android.view.ViewGroup; 33 import com.android.dialer.calllog.CallLogComponent; 34 import com.android.dialer.calllog.RefreshAnnotatedCallLogReceiver; 35 import com.android.dialer.calllog.database.CallLogDatabaseComponent; 36 import com.android.dialer.calllog.database.Coalescer; 37 import com.android.dialer.calllog.model.CoalescedRow; 38 import com.android.dialer.common.Assert; 39 import com.android.dialer.common.LogUtil; 40 import com.android.dialer.common.concurrent.DefaultFutureCallback; 41 import com.android.dialer.common.concurrent.DialerExecutorComponent; 42 import com.android.dialer.common.concurrent.SupportUiListener; 43 import com.android.dialer.common.concurrent.ThreadUtil; 44 import com.android.dialer.metrics.Metrics; 45 import com.android.dialer.metrics.MetricsComponent; 46 import com.android.dialer.metrics.jank.RecyclerViewJankLogger; 47 import com.android.dialer.promotion.Promotion.PromotionType; 48 import com.android.dialer.promotion.PromotionComponent; 49 import com.android.dialer.util.PermissionsUtil; 50 import com.android.dialer.widget.EmptyContentView; 51 import com.android.dialer.widget.EmptyContentView.OnEmptyViewActionButtonClickedListener; 52 import com.google.common.collect.ImmutableList; 53 import com.google.common.util.concurrent.Futures; 54 import com.google.common.util.concurrent.ListenableFuture; 55 import com.google.common.util.concurrent.MoreExecutors; 56 import java.util.Arrays; 57 import java.util.concurrent.TimeUnit; 58 59 /** The "new" call log fragment implementation, which is built on top of the annotated call log. */ 60 public final class NewCallLogFragment extends Fragment implements LoaderCallbacks<Cursor> { 61 62 private static final int PHONE_PERMISSIONS_REQUEST_CODE = 1; 63 private static final int LOADER_ID = 0; 64 65 @VisibleForTesting 66 static final long MARK_ALL_CALLS_READ_WAIT_MILLIS = TimeUnit.SECONDS.toMillis(3); 67 68 private RecyclerView recyclerView; 69 private EmptyContentView emptyContentView; 70 private RefreshAnnotatedCallLogReceiver refreshAnnotatedCallLogReceiver; 71 private SupportUiListener<ImmutableList<CoalescedRow>> coalesingAnnotatedCallLogListener; 72 73 private boolean shouldMarkCallsRead = false; 74 private final Runnable setShouldMarkCallsReadTrue = () -> shouldMarkCallsRead = true; 75 NewCallLogFragment()76 public NewCallLogFragment() { 77 LogUtil.enterBlock("NewCallLogFragment.NewCallLogFragment"); 78 } 79 80 @Override onActivityCreated(@ullable Bundle savedInstanceState)81 public void onActivityCreated(@Nullable Bundle savedInstanceState) { 82 super.onActivityCreated(savedInstanceState); 83 84 LogUtil.enterBlock("NewCallLogFragment.onActivityCreated"); 85 86 refreshAnnotatedCallLogReceiver = new RefreshAnnotatedCallLogReceiver(getContext()); 87 } 88 89 @Override onStart()90 public void onStart() { 91 super.onStart(); 92 93 LogUtil.enterBlock("NewCallLogFragment.onStart"); 94 } 95 96 @Override onResume()97 public void onResume() { 98 super.onResume(); 99 100 boolean isHidden = isHidden(); 101 LogUtil.i("NewCallLogFragment.onResume", "isHidden = %s", isHidden); 102 103 // As a fragment's onResume() is tied to the containing Activity's onResume(), being resumed is 104 // not equivalent to becoming visible. 105 // For example, when an activity with a hidden fragment is resumed, the fragment's onResume() 106 // will be called but it is not visible. 107 if (!isHidden) { 108 onFragmentShown(); 109 } 110 } 111 112 @Override onStop()113 public void onStop() { 114 super.onStop(); 115 116 if (recyclerView.getAdapter() != null) { 117 ((NewCallLogAdapter) recyclerView.getAdapter()).logMetrics(getContext()); 118 } 119 } 120 121 @Override onPause()122 public void onPause() { 123 super.onPause(); 124 LogUtil.enterBlock("NewCallLogFragment.onPause"); 125 126 onFragmentHidden(); 127 } 128 129 @Override onHiddenChanged(boolean hidden)130 public void onHiddenChanged(boolean hidden) { 131 super.onHiddenChanged(hidden); 132 LogUtil.i("NewCallLogFragment.onHiddenChanged", "hidden = %s", hidden); 133 134 if (hidden) { 135 onFragmentHidden(); 136 } else { 137 onFragmentShown(); 138 } 139 } 140 141 /** 142 * To be called when the fragment becomes visible. 143 * 144 * <p>Note that for a fragment, being resumed is not equivalent to becoming visible. 145 * 146 * <p>For example, when an activity with a hidden fragment is resumed, the fragment's onResume() 147 * will be called but it is not visible. 148 */ onFragmentShown()149 private void onFragmentShown() { 150 LoaderManager loaderManager = getLoaderManager(); 151 if (!PermissionsUtil.hasCallLogReadPermissions(getContext())) { 152 recyclerView.setVisibility(View.GONE); 153 emptyContentView.setVisibility(View.VISIBLE); 154 loaderManager.destroyLoader(LOADER_ID); 155 return; 156 } 157 158 recyclerView.setVisibility(View.VISIBLE); 159 emptyContentView.setVisibility(View.GONE); 160 161 // This can happen if permissions were not enabled when the fragment was created. 162 if (loaderManager.getLoader(LOADER_ID) == null) { 163 loaderManager.restartLoader(LOADER_ID, null, this); 164 } 165 166 registerRefreshAnnotatedCallLogReceiver(); 167 168 CallLogComponent.get(getContext()) 169 .getRefreshAnnotatedCallLogNotifier() 170 .notify(/* checkDirty = */ true); 171 172 // There are some types of data that we show in the call log that are not represented in the 173 // AnnotatedCallLog. For example, CP2 information for invalid numbers can sometimes only be 174 // fetched at display time. Because of this, we need to clear the adapter's cache and update it 175 // whenever the user arrives at the call log (rather than relying on changes to the CursorLoader 176 // alone). 177 if (recyclerView.getAdapter() != null) { 178 ((NewCallLogAdapter) recyclerView.getAdapter()).clearCache(); 179 recyclerView.getAdapter().notifyDataSetChanged(); 180 } 181 182 // We shouldn't mark the calls as read immediately when the 3 second timer expires because we 183 // don't want to disrupt the UI; instead we set a bit indicating to mark them read when the user 184 // leaves the fragment (in onPause). 185 shouldMarkCallsRead = false; 186 ThreadUtil.getUiThreadHandler() 187 .postDelayed(setShouldMarkCallsReadTrue, MARK_ALL_CALLS_READ_WAIT_MILLIS); 188 } 189 190 /** 191 * To be called when the fragment becomes hidden. 192 * 193 * <p>This can happen in the following two cases: 194 * 195 * <ul> 196 * <li>hide the fragment but keep the parent activity visible (e.g., calling {@link 197 * android.support.v4.app.FragmentTransaction#hide(Fragment)} in an activity, or 198 * <li>the parent activity is paused. 199 * </ul> 200 */ onFragmentHidden()201 private void onFragmentHidden() { 202 // This is pending work that we don't actually need to follow through with. 203 ThreadUtil.getUiThreadHandler().removeCallbacks(setShouldMarkCallsReadTrue); 204 205 unregisterRefreshAnnotatedCallLogReceiver(); 206 207 if (shouldMarkCallsRead) { 208 Futures.addCallback( 209 CallLogComponent.get(getContext()).getClearMissedCalls().clearAll(), 210 new DefaultFutureCallback<>(), 211 MoreExecutors.directExecutor()); 212 } 213 } 214 215 @Override onCreateView( LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)216 public View onCreateView( 217 LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { 218 LogUtil.enterBlock("NewCallLogFragment.onCreateView"); 219 220 View view = inflater.inflate(R.layout.new_call_log_fragment, container, false); 221 recyclerView = view.findViewById(R.id.new_call_log_recycler_view); 222 recyclerView.addOnScrollListener( 223 new RecyclerViewJankLogger( 224 MetricsComponent.get(getContext()).metrics(), Metrics.NEW_CALL_LOG_JANK_EVENT_NAME)); 225 226 emptyContentView = view.findViewById(R.id.new_call_log_empty_content_view); 227 configureEmptyContentView(); 228 229 coalesingAnnotatedCallLogListener = 230 DialerExecutorComponent.get(getContext()) 231 .createUiListener( 232 getChildFragmentManager(), 233 /* taskId = */ "NewCallLogFragment.coalescingAnnotatedCallLog"); 234 235 if (PermissionsUtil.hasCallLogReadPermissions(getContext())) { 236 getLoaderManager().restartLoader(LOADER_ID, null, this); 237 } 238 239 return view; 240 } 241 configureEmptyContentView()242 private void configureEmptyContentView() { 243 emptyContentView.setImage(R.drawable.quantum_ic_query_builder_vd_theme_24); 244 emptyContentView.setDescription(R.string.new_call_log_permission_no_calllog); 245 emptyContentView.setActionLabel(com.android.dialer.widget.R.string.permission_single_turn_on); 246 emptyContentView.setActionClickedListener(new TurnOnPhonePermissions()); 247 } 248 249 private class TurnOnPhonePermissions implements OnEmptyViewActionButtonClickedListener { 250 251 @Override onEmptyViewActionButtonClicked()252 public void onEmptyViewActionButtonClicked() { 253 if (getContext() == null) { 254 LogUtil.w("TurnOnPhonePermissions.onEmptyViewActionButtonClicked", "no context"); 255 return; 256 } 257 String[] deniedPermissions = 258 PermissionsUtil.getPermissionsCurrentlyDenied( 259 getContext(), PermissionsUtil.allPhoneGroupPermissionsUsedInDialer); 260 if (deniedPermissions.length > 0) { 261 LogUtil.i( 262 "TurnOnPhonePermissions.onEmptyViewActionButtonClicked", 263 "requesting permissions: %s", 264 Arrays.toString(deniedPermissions)); 265 // Don't implement onRequestPermissionsResult; instead rely on views being updated in 266 // #onFragmentShown. 267 requestPermissions(deniedPermissions, PHONE_PERMISSIONS_REQUEST_CODE); 268 } 269 } 270 } 271 registerRefreshAnnotatedCallLogReceiver()272 private void registerRefreshAnnotatedCallLogReceiver() { 273 LogUtil.enterBlock("NewCallLogFragment.registerRefreshAnnotatedCallLogReceiver"); 274 275 LocalBroadcastManager.getInstance(getContext()) 276 .registerReceiver( 277 refreshAnnotatedCallLogReceiver, RefreshAnnotatedCallLogReceiver.getIntentFilter()); 278 } 279 unregisterRefreshAnnotatedCallLogReceiver()280 private void unregisterRefreshAnnotatedCallLogReceiver() { 281 LogUtil.enterBlock("NewCallLogFragment.unregisterRefreshAnnotatedCallLogReceiver"); 282 283 // Cancel pending work as we don't need it any more. 284 CallLogComponent.get(getContext()).getRefreshAnnotatedCallLogNotifier().cancel(); 285 286 LocalBroadcastManager.getInstance(getContext()) 287 .unregisterReceiver(refreshAnnotatedCallLogReceiver); 288 } 289 290 @Override onCreateLoader(int id, Bundle args)291 public Loader<Cursor> onCreateLoader(int id, Bundle args) { 292 LogUtil.enterBlock("NewCallLogFragment.onCreateLoader"); 293 return new AnnotatedCallLogCursorLoader(Assert.isNotNull(getContext())); 294 } 295 296 @Override onLoadFinished(Loader<Cursor> loader, Cursor newCursor)297 public void onLoadFinished(Loader<Cursor> loader, Cursor newCursor) { 298 LogUtil.enterBlock("NewCallLogFragment.onLoadFinished"); 299 300 if (newCursor == null) { 301 // This might be possible when the annotated call log hasn't been created but we're trying 302 // to show the call log. 303 LogUtil.w("NewCallLogFragment.onLoadFinished", "null cursor"); 304 return; 305 } 306 307 // Start combining adjacent rows which should be collapsed for display purposes. 308 // This is a time-consuming process so we will do it in the background. 309 ListenableFuture<ImmutableList<CoalescedRow>> coalescedRowsFuture = 310 CallLogDatabaseComponent.get(getContext()).coalescer().coalesce(newCursor); 311 312 coalesingAnnotatedCallLogListener.listen( 313 getContext(), 314 coalescedRowsFuture, 315 coalescedRows -> { 316 LogUtil.i("NewCallLogFragment.onLoadFinished", "coalescing succeeded"); 317 318 // TODO(zachh): Handle empty cursor by showing empty view. 319 if (recyclerView.getAdapter() == null) { 320 recyclerView.setLayoutManager(new LinearLayoutManager(getContext())); 321 // Note: It's not clear if this callback can be invoked when there's no associated 322 // activity, but if crashes are observed here it may be possible to use getContext() 323 // instead. 324 Activity activity = Assert.isNotNull(getActivity()); 325 recyclerView.setAdapter( 326 new NewCallLogAdapter( 327 activity, 328 coalescedRows, 329 System::currentTimeMillis, 330 PromotionComponent.get(getContext()) 331 .promotionManager() 332 .getHighestPriorityPromotion(PromotionType.CARD) 333 .orElse(null))); 334 } else { 335 ((NewCallLogAdapter) recyclerView.getAdapter()).updateRows(coalescedRows); 336 } 337 }, 338 throwable -> { 339 // Coalescing can fail if the cursor passed to Coalescer is closed by the loader while 340 // the work is still in progress. 341 // This can happen when the loader restarts and finishes loading data before the 342 // coalescing work is completed. 343 // This failure is identified by ExpectedCoalescerException and doesn't need to be 344 // thrown as coalescing will be restarted on the latest data obtained by the loader. 345 if (!(throwable instanceof Coalescer.ExpectedCoalescerException)) { 346 throw new AssertionError(throwable); 347 } 348 }); 349 } 350 351 @Override onLoaderReset(Loader<Cursor> loader)352 public void onLoaderReset(Loader<Cursor> loader) { 353 LogUtil.enterBlock("NewCallLogFragment.onLoaderReset"); 354 recyclerView.setAdapter(null); 355 } 356 } 357