Loading java/com/android/dialer/app/calllog/CallLogAdapter.java +8 −2 Original line number Diff line number Diff line Loading @@ -100,6 +100,8 @@ import com.google.i18n.phonenumbers.Phonenumber.PhoneNumber; import java.util.ArrayList; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; /** Adapter class to fill in data for the Call Log. */ public class CallLogAdapter extends GroupingListAdapter Loading Loading @@ -158,9 +160,12 @@ public class CallLogAdapter extends GroupingListAdapter /** * Maps a raw input number to match info. We only log one MatchInfo per raw input number to reduce * the amount of data logged. * * <p>Note that this has to be a {@link ConcurrentMap} as the match info for each row in the UI is * loaded in a background thread spawned when the ViewHolder is bound. */ private final Map<String, ContactsProviderMatchInfo> contactsProviderMatchInfos = new ArrayMap<>(); private final ConcurrentMap<String, ContactsProviderMatchInfo> contactsProviderMatchInfos = new ConcurrentHashMap<>(); private final ActionMode.Callback actionModeCallback = new ActionMode.Callback() { Loading Loading @@ -1464,6 +1469,7 @@ public class CallLogAdapter extends GroupingListAdapter notifyDataSetChanged(); } @WorkerThread private void logCp2Metrics(PhoneCallDetails details, ContactInfo contactInfo) { if (details == null) { return; Loading java/com/android/dialer/calllog/CallLogCacheUpdater.java +26 −4 Original line number Diff line number Diff line Loading @@ -25,6 +25,7 @@ import android.os.RemoteException; import android.provider.CallLog; import android.provider.CallLog.Calls; import android.provider.ContactsContract.CommonDataKinds.Phone; import android.support.annotation.VisibleForTesting; import com.android.dialer.DialerPhoneNumber; import com.android.dialer.NumberAttributes; import com.android.dialer.calllog.database.contract.AnnotatedCallLogContract.AnnotatedCallLog; Loading @@ -33,6 +34,7 @@ import com.android.dialer.common.LogUtil; import com.android.dialer.common.concurrent.Annotations.BackgroundExecutor; import com.android.dialer.inject.ApplicationContext; import com.android.dialer.protos.ProtoParsers; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; import java.util.ArrayList; Loading @@ -49,13 +51,23 @@ public final class CallLogCacheUpdater { private final Context appContext; private final ListeningExecutorService backgroundExecutor; private final CallLogState callLogState; /** * Maximum numbers of operations the updater can do. Each transaction to the system call log will * trigger a call log refresh, so the updater can only do a single batch. If there are more * operations it will be truncated. Under normal circumstances there will only be 1 operation */ @VisibleForTesting static final int CACHE_UPDATE_LIMIT = 100; @Inject CallLogCacheUpdater( @ApplicationContext Context appContext, @BackgroundExecutor ListeningExecutorService backgroundExecutor) { @BackgroundExecutor ListeningExecutorService backgroundExecutor, CallLogState callLogState) { this.appContext = appContext; this.backgroundExecutor = backgroundExecutor; this.callLogState = callLogState; } /** Loading @@ -66,17 +78,27 @@ public final class CallLogCacheUpdater { * has changed */ public ListenableFuture<Void> updateCache(CallLogMutations mutations) { return backgroundExecutor.submit( () -> { return Futures.transform( callLogState.isBuilt(), isBuilt -> { if (!isBuilt) { // Initial build might need to update 1000 caches, which may overflow the batch // operation limit. The initial data was already built with the cache, there's no need // to update it. LogUtil.i("CallLogCacheUpdater.updateCache", "not updating cache for initial build"); return null; } updateCacheInternal(mutations); return null; }); }, backgroundExecutor); } private void updateCacheInternal(CallLogMutations mutations) { ArrayList<ContentProviderOperation> operations = new ArrayList<>(); Stream.concat( mutations.getInserts().entrySet().stream(), mutations.getUpdates().entrySet().stream()) .limit(CACHE_UPDATE_LIMIT) .forEach( entry -> { ContentValues values = entry.getValue(); Loading java/com/android/dialer/calllog/database/Coalescer.java +65 −19 Original line number Diff line number Diff line Loading @@ -16,6 +16,7 @@ package com.android.dialer.calllog.database; import android.database.Cursor; import android.database.StaleDataException; import android.provider.CallLog.Calls; import android.support.annotation.NonNull; import android.support.annotation.WorkerThread; Loading Loading @@ -85,15 +86,16 @@ public class Coalescer { @WorkerThread @NonNull private ImmutableList<CoalescedRow> coalesceInternal( Cursor allAnnotatedCallLogRowsSortedByTimestampDesc) { Cursor allAnnotatedCallLogRowsSortedByTimestampDesc) throws ExpectedCoalescerException { Assert.isWorkerThread(); ImmutableList.Builder<CoalescedRow> coalescedRowListBuilder = new ImmutableList.Builder<>(); try { if (!allAnnotatedCallLogRowsSortedByTimestampDesc.moveToFirst()) { return ImmutableList.of(); } ImmutableList.Builder<CoalescedRow> coalescedRowListBuilder = new ImmutableList.Builder<>(); RowCombiner rowCombiner = new RowCombiner(allAnnotatedCallLogRowsSortedByTimestampDesc); rowCombiner.startNewGroup(); Loading @@ -113,6 +115,43 @@ public class Coalescer { } while (!allAnnotatedCallLogRowsSortedByTimestampDesc.isAfterLast()); return coalescedRowListBuilder.build(); } catch (Exception exception) { // Coalescing can fail if cursor "allAnnotatedCallLogRowsSortedByTimestampDesc" is closed by // its loader while the work is still in progress. // // This can happen when the loader restarts and finishes loading data before the coalescing // work is completed. // // This kind of failure doesn't have to crash the app as coalescing will be restarted on the // latest data obtained by the loader. Therefore, we inspect the exception here and throw an // ExpectedCoalescerException if it is the case described above. // // The type of expected exception depends on whether AbstractWindowedCursor#checkPosition() is // called when the cursor is closed. // (1) If it is called before the cursor is closed, we will get IllegalStateException thrown // by SQLiteClosable when it attempts to acquire a reference to the database. // (2) Otherwise, we will get StaleDataException thrown by AbstractWindowedCursor's // checkPosition() method. // // Note that it would be more accurate to inspect the stack trace to locate the origin of the // exception. However, according to the documentation on Throwable#getStackTrace, "some // virtual machines may, under some circumstances, omit one or more stack frames from the // stack trace". "In the extreme case, a virtual machine that has no stack trace information // concerning this throwable is permitted to return a zero-length array from this method." // Therefore, the best we can do is to inspect the message in the exception. // TODO(linyuh): try to avoid the expected failure. String message = exception.getMessage(); if (message != null && ((exception instanceof StaleDataException && message.startsWith("Attempting to access a closed CursorWindow")) || (exception instanceof IllegalStateException && message.startsWith("attempt to re-open an already-closed object")))) { throw new ExpectedCoalescerException(exception); } throw exception; } } /** Combines rows from {@link AnnotatedCallLog} into a {@link CoalescedRow}. */ Loading Loading @@ -337,4 +376,11 @@ public class Coalescer { return dialerPhoneNumberUtil.isMatch(groupPhoneNumber, rowPhoneNumber); } } /** A checked exception thrown when expected failure happens when coalescing is in progress. */ public static final class ExpectedCoalescerException extends Exception { ExpectedCoalescerException(Throwable throwable) { super("Expected coalescing exception", throwable); } } } java/com/android/dialer/calllog/ui/NewCallLogFragment.java +8 −11 Original line number Diff line number Diff line Loading @@ -17,7 +17,6 @@ package com.android.dialer.calllog.ui; import android.app.Activity; import android.database.Cursor; import android.database.StaleDataException; import android.os.Bundle; import android.support.annotation.Nullable; import android.support.annotation.VisibleForTesting; Loading @@ -34,6 +33,7 @@ import android.view.ViewGroup; import com.android.dialer.calllog.CallLogComponent; import com.android.dialer.calllog.RefreshAnnotatedCallLogReceiver; import com.android.dialer.calllog.database.CallLogDatabaseComponent; import com.android.dialer.calllog.database.Coalescer; import com.android.dialer.calllog.model.CoalescedRow; import com.android.dialer.common.Assert; import com.android.dialer.common.LogUtil; Loading Loading @@ -337,16 +337,13 @@ public final class NewCallLogFragment extends Fragment implements LoaderCallback } }, throwable -> { if (throwable instanceof StaleDataException) { // Coalescing can fail if the cursor passed to Coalescer is closed by the loader while // the work is still in progress. // This can happen when the loader restarts and finishes loading data before the // coalescing work is completed. // This failure doesn't need to be thrown as coalescing will be restarted on the latest // data obtained by the loader. // TODO(linyuh): Also throw an exception if the failure above can be avoided. LogUtil.e("NewCallLogFragment.onLoadFinished", "coalescing failed", throwable); } else { // This failure is identified by ExpectedCoalescerException and doesn't need to be // thrown as coalescing will be restarted on the latest data obtained by the loader. if (!(throwable instanceof Coalescer.ExpectedCoalescerException)) { throw new AssertionError(throwable); } }); Loading java/com/android/dialer/main/impl/OldMainActivityPeer.java +26 −3 Original line number Diff line number Diff line Loading @@ -51,6 +51,7 @@ import android.text.method.LinkMovementMethod; import android.view.ActionMode; import android.view.DragEvent; import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; import android.widget.TextView; import com.android.contacts.common.list.OnPhoneNumberPickerActionListener; Loading Loading @@ -334,7 +335,12 @@ public class OldMainActivityPeer implements MainActivityPeer, FragmentUtilListen activity.findViewById(R.id.remove_view), activity.findViewById(R.id.search_view_container), toolbar); speedDialFragmentHost = new MainSpeedDialFragmentHost(toolbar); speedDialFragmentHost = new MainSpeedDialFragmentHost( toolbar, activity.findViewById(R.id.root_layout), activity.findViewById(R.id.coordinator_layout), activity.findViewById(R.id.fragment_container)); lastTabController = new LastTabController(activity, bottomNav, showVoicemailTab); Loading Loading @@ -1255,15 +1261,32 @@ public class OldMainActivityPeer implements MainActivityPeer, FragmentUtilListen private static final class MainSpeedDialFragmentHost implements SpeedDialFragment.HostInterface { private final MainToolbar toolbar; MainSpeedDialFragmentHost(MainToolbar toolbar) { private final ViewGroup rootLayout; private final ViewGroup coordinatorLayout; private final ViewGroup fragmentContainer; MainSpeedDialFragmentHost( MainToolbar toolbar, ViewGroup rootLayout, ViewGroup coordinatorLayout, ViewGroup fragmentContainer) { this.toolbar = toolbar; this.rootLayout = rootLayout; this.coordinatorLayout = coordinatorLayout; this.fragmentContainer = fragmentContainer; } @Override public void setHasFrequents(boolean hasFrequents) { toolbar.showClearFrequents(hasFrequents); } @Override public void dragFavorite(boolean start) { rootLayout.setClipChildren(!start); coordinatorLayout.setClipChildren(!start); fragmentContainer.setClipChildren(!start); } } /** Loading Loading
java/com/android/dialer/app/calllog/CallLogAdapter.java +8 −2 Original line number Diff line number Diff line Loading @@ -100,6 +100,8 @@ import com.google.i18n.phonenumbers.Phonenumber.PhoneNumber; import java.util.ArrayList; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; /** Adapter class to fill in data for the Call Log. */ public class CallLogAdapter extends GroupingListAdapter Loading Loading @@ -158,9 +160,12 @@ public class CallLogAdapter extends GroupingListAdapter /** * Maps a raw input number to match info. We only log one MatchInfo per raw input number to reduce * the amount of data logged. * * <p>Note that this has to be a {@link ConcurrentMap} as the match info for each row in the UI is * loaded in a background thread spawned when the ViewHolder is bound. */ private final Map<String, ContactsProviderMatchInfo> contactsProviderMatchInfos = new ArrayMap<>(); private final ConcurrentMap<String, ContactsProviderMatchInfo> contactsProviderMatchInfos = new ConcurrentHashMap<>(); private final ActionMode.Callback actionModeCallback = new ActionMode.Callback() { Loading Loading @@ -1464,6 +1469,7 @@ public class CallLogAdapter extends GroupingListAdapter notifyDataSetChanged(); } @WorkerThread private void logCp2Metrics(PhoneCallDetails details, ContactInfo contactInfo) { if (details == null) { return; Loading
java/com/android/dialer/calllog/CallLogCacheUpdater.java +26 −4 Original line number Diff line number Diff line Loading @@ -25,6 +25,7 @@ import android.os.RemoteException; import android.provider.CallLog; import android.provider.CallLog.Calls; import android.provider.ContactsContract.CommonDataKinds.Phone; import android.support.annotation.VisibleForTesting; import com.android.dialer.DialerPhoneNumber; import com.android.dialer.NumberAttributes; import com.android.dialer.calllog.database.contract.AnnotatedCallLogContract.AnnotatedCallLog; Loading @@ -33,6 +34,7 @@ import com.android.dialer.common.LogUtil; import com.android.dialer.common.concurrent.Annotations.BackgroundExecutor; import com.android.dialer.inject.ApplicationContext; import com.android.dialer.protos.ProtoParsers; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; import java.util.ArrayList; Loading @@ -49,13 +51,23 @@ public final class CallLogCacheUpdater { private final Context appContext; private final ListeningExecutorService backgroundExecutor; private final CallLogState callLogState; /** * Maximum numbers of operations the updater can do. Each transaction to the system call log will * trigger a call log refresh, so the updater can only do a single batch. If there are more * operations it will be truncated. Under normal circumstances there will only be 1 operation */ @VisibleForTesting static final int CACHE_UPDATE_LIMIT = 100; @Inject CallLogCacheUpdater( @ApplicationContext Context appContext, @BackgroundExecutor ListeningExecutorService backgroundExecutor) { @BackgroundExecutor ListeningExecutorService backgroundExecutor, CallLogState callLogState) { this.appContext = appContext; this.backgroundExecutor = backgroundExecutor; this.callLogState = callLogState; } /** Loading @@ -66,17 +78,27 @@ public final class CallLogCacheUpdater { * has changed */ public ListenableFuture<Void> updateCache(CallLogMutations mutations) { return backgroundExecutor.submit( () -> { return Futures.transform( callLogState.isBuilt(), isBuilt -> { if (!isBuilt) { // Initial build might need to update 1000 caches, which may overflow the batch // operation limit. The initial data was already built with the cache, there's no need // to update it. LogUtil.i("CallLogCacheUpdater.updateCache", "not updating cache for initial build"); return null; } updateCacheInternal(mutations); return null; }); }, backgroundExecutor); } private void updateCacheInternal(CallLogMutations mutations) { ArrayList<ContentProviderOperation> operations = new ArrayList<>(); Stream.concat( mutations.getInserts().entrySet().stream(), mutations.getUpdates().entrySet().stream()) .limit(CACHE_UPDATE_LIMIT) .forEach( entry -> { ContentValues values = entry.getValue(); Loading
java/com/android/dialer/calllog/database/Coalescer.java +65 −19 Original line number Diff line number Diff line Loading @@ -16,6 +16,7 @@ package com.android.dialer.calllog.database; import android.database.Cursor; import android.database.StaleDataException; import android.provider.CallLog.Calls; import android.support.annotation.NonNull; import android.support.annotation.WorkerThread; Loading Loading @@ -85,15 +86,16 @@ public class Coalescer { @WorkerThread @NonNull private ImmutableList<CoalescedRow> coalesceInternal( Cursor allAnnotatedCallLogRowsSortedByTimestampDesc) { Cursor allAnnotatedCallLogRowsSortedByTimestampDesc) throws ExpectedCoalescerException { Assert.isWorkerThread(); ImmutableList.Builder<CoalescedRow> coalescedRowListBuilder = new ImmutableList.Builder<>(); try { if (!allAnnotatedCallLogRowsSortedByTimestampDesc.moveToFirst()) { return ImmutableList.of(); } ImmutableList.Builder<CoalescedRow> coalescedRowListBuilder = new ImmutableList.Builder<>(); RowCombiner rowCombiner = new RowCombiner(allAnnotatedCallLogRowsSortedByTimestampDesc); rowCombiner.startNewGroup(); Loading @@ -113,6 +115,43 @@ public class Coalescer { } while (!allAnnotatedCallLogRowsSortedByTimestampDesc.isAfterLast()); return coalescedRowListBuilder.build(); } catch (Exception exception) { // Coalescing can fail if cursor "allAnnotatedCallLogRowsSortedByTimestampDesc" is closed by // its loader while the work is still in progress. // // This can happen when the loader restarts and finishes loading data before the coalescing // work is completed. // // This kind of failure doesn't have to crash the app as coalescing will be restarted on the // latest data obtained by the loader. Therefore, we inspect the exception here and throw an // ExpectedCoalescerException if it is the case described above. // // The type of expected exception depends on whether AbstractWindowedCursor#checkPosition() is // called when the cursor is closed. // (1) If it is called before the cursor is closed, we will get IllegalStateException thrown // by SQLiteClosable when it attempts to acquire a reference to the database. // (2) Otherwise, we will get StaleDataException thrown by AbstractWindowedCursor's // checkPosition() method. // // Note that it would be more accurate to inspect the stack trace to locate the origin of the // exception. However, according to the documentation on Throwable#getStackTrace, "some // virtual machines may, under some circumstances, omit one or more stack frames from the // stack trace". "In the extreme case, a virtual machine that has no stack trace information // concerning this throwable is permitted to return a zero-length array from this method." // Therefore, the best we can do is to inspect the message in the exception. // TODO(linyuh): try to avoid the expected failure. String message = exception.getMessage(); if (message != null && ((exception instanceof StaleDataException && message.startsWith("Attempting to access a closed CursorWindow")) || (exception instanceof IllegalStateException && message.startsWith("attempt to re-open an already-closed object")))) { throw new ExpectedCoalescerException(exception); } throw exception; } } /** Combines rows from {@link AnnotatedCallLog} into a {@link CoalescedRow}. */ Loading Loading @@ -337,4 +376,11 @@ public class Coalescer { return dialerPhoneNumberUtil.isMatch(groupPhoneNumber, rowPhoneNumber); } } /** A checked exception thrown when expected failure happens when coalescing is in progress. */ public static final class ExpectedCoalescerException extends Exception { ExpectedCoalescerException(Throwable throwable) { super("Expected coalescing exception", throwable); } } }
java/com/android/dialer/calllog/ui/NewCallLogFragment.java +8 −11 Original line number Diff line number Diff line Loading @@ -17,7 +17,6 @@ package com.android.dialer.calllog.ui; import android.app.Activity; import android.database.Cursor; import android.database.StaleDataException; import android.os.Bundle; import android.support.annotation.Nullable; import android.support.annotation.VisibleForTesting; Loading @@ -34,6 +33,7 @@ import android.view.ViewGroup; import com.android.dialer.calllog.CallLogComponent; import com.android.dialer.calllog.RefreshAnnotatedCallLogReceiver; import com.android.dialer.calllog.database.CallLogDatabaseComponent; import com.android.dialer.calllog.database.Coalescer; import com.android.dialer.calllog.model.CoalescedRow; import com.android.dialer.common.Assert; import com.android.dialer.common.LogUtil; Loading Loading @@ -337,16 +337,13 @@ public final class NewCallLogFragment extends Fragment implements LoaderCallback } }, throwable -> { if (throwable instanceof StaleDataException) { // Coalescing can fail if the cursor passed to Coalescer is closed by the loader while // the work is still in progress. // This can happen when the loader restarts and finishes loading data before the // coalescing work is completed. // This failure doesn't need to be thrown as coalescing will be restarted on the latest // data obtained by the loader. // TODO(linyuh): Also throw an exception if the failure above can be avoided. LogUtil.e("NewCallLogFragment.onLoadFinished", "coalescing failed", throwable); } else { // This failure is identified by ExpectedCoalescerException and doesn't need to be // thrown as coalescing will be restarted on the latest data obtained by the loader. if (!(throwable instanceof Coalescer.ExpectedCoalescerException)) { throw new AssertionError(throwable); } }); Loading
java/com/android/dialer/main/impl/OldMainActivityPeer.java +26 −3 Original line number Diff line number Diff line Loading @@ -51,6 +51,7 @@ import android.text.method.LinkMovementMethod; import android.view.ActionMode; import android.view.DragEvent; import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; import android.widget.TextView; import com.android.contacts.common.list.OnPhoneNumberPickerActionListener; Loading Loading @@ -334,7 +335,12 @@ public class OldMainActivityPeer implements MainActivityPeer, FragmentUtilListen activity.findViewById(R.id.remove_view), activity.findViewById(R.id.search_view_container), toolbar); speedDialFragmentHost = new MainSpeedDialFragmentHost(toolbar); speedDialFragmentHost = new MainSpeedDialFragmentHost( toolbar, activity.findViewById(R.id.root_layout), activity.findViewById(R.id.coordinator_layout), activity.findViewById(R.id.fragment_container)); lastTabController = new LastTabController(activity, bottomNav, showVoicemailTab); Loading Loading @@ -1255,15 +1261,32 @@ public class OldMainActivityPeer implements MainActivityPeer, FragmentUtilListen private static final class MainSpeedDialFragmentHost implements SpeedDialFragment.HostInterface { private final MainToolbar toolbar; MainSpeedDialFragmentHost(MainToolbar toolbar) { private final ViewGroup rootLayout; private final ViewGroup coordinatorLayout; private final ViewGroup fragmentContainer; MainSpeedDialFragmentHost( MainToolbar toolbar, ViewGroup rootLayout, ViewGroup coordinatorLayout, ViewGroup fragmentContainer) { this.toolbar = toolbar; this.rootLayout = rootLayout; this.coordinatorLayout = coordinatorLayout; this.fragmentContainer = fragmentContainer; } @Override public void setHasFrequents(boolean hasFrequents) { toolbar.showClearFrequents(hasFrequents); } @Override public void dragFavorite(boolean start) { rootLayout.setClipChildren(!start); coordinatorLayout.setClipChildren(!start); fragmentContainer.setClipChildren(!start); } } /** Loading