/* * Copyright (C) 2018 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License */ package com.android.dialer.calllog; import android.annotation.SuppressLint; import android.app.job.JobInfo; import android.app.job.JobParameters; import android.app.job.JobScheduler; import android.app.job.JobService; import android.content.ComponentName; import android.content.Context; import android.content.SharedPreferences; import android.support.v4.os.UserManagerCompat; import com.android.dialer.common.Assert; import com.android.dialer.common.LogUtil; import com.android.dialer.common.concurrent.Annotations.BackgroundExecutor; import com.android.dialer.common.concurrent.Annotations.LightweightExecutor; import com.android.dialer.common.concurrent.ThreadUtil; import com.android.dialer.configprovider.ConfigProvider; import com.android.dialer.constants.ScheduledJobIds; import com.android.dialer.storage.Unencrypted; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.MoreExecutors; import java.util.concurrent.TimeUnit; import javax.inject.Inject; /** * Determines if new call log components are enabled. * *

When the underlying flag values from the {@link ConfigProvider} changes, it is necessary to do * work such as registering/unregistering content observers, and this class is responsible for * coordinating that work. * *

New UI application components should use this class instead of reading flags directly from the * {@link ConfigProvider}. */ public final class CallLogConfig { private static final String NEW_CALL_LOG_FRAGMENT_ENABLED_PREF_KEY = "newCallLogFragmentEnabled"; private static final String NEW_VOICEMAIL_FRAGMENT_ENABLED_PREF_KEY = "newVoicemailFragmentEnabled"; private static final String NEW_PEER_ENABLED_PREF_KEY = "newPeerEnabled"; private static final String NEW_CALL_LOG_FRAMEWORK_ENABLED_PREF_KEY = "newCallLogFrameworkEnabled"; private final CallLogFramework callLogFramework; private final SharedPreferences sharedPreferences; private final ConfigProvider configProvider; private final AnnotatedCallLogMigrator annotatedCallLogMigrator; private final ListeningExecutorService backgroundExecutor; private final ListeningExecutorService lightweightExecutor; @Inject public CallLogConfig( CallLogFramework callLogFramework, @Unencrypted SharedPreferences sharedPreferences, ConfigProvider configProvider, AnnotatedCallLogMigrator annotatedCallLogMigrator, @BackgroundExecutor ListeningExecutorService backgroundExecutor, @LightweightExecutor ListeningExecutorService lightweightExecutor) { this.callLogFramework = callLogFramework; this.sharedPreferences = sharedPreferences; this.configProvider = configProvider; this.annotatedCallLogMigrator = annotatedCallLogMigrator; this.backgroundExecutor = backgroundExecutor; this.lightweightExecutor = lightweightExecutor; } /** * Updates the config values. This may kick off a lot of work so should be done infrequently, for * example by a scheduled job or broadcast receiver which rarely fires. */ public ListenableFuture update() { boolean newCallLogFragmentEnabledInConfigProvider = configProvider.getBoolean("new_call_log_fragment_enabled", false); boolean newVoicemailFragmentEnabledInConfigProvider = configProvider.getBoolean("new_voicemail_fragment_enabled", false); boolean newPeerEnabledInConfigProvider = configProvider.getBoolean("nui_peer_enabled", false); boolean isCallLogFrameworkEnabled = isCallLogFrameworkEnabled(); boolean callLogFrameworkShouldBeEnabled = newCallLogFragmentEnabledInConfigProvider || newVoicemailFragmentEnabledInConfigProvider || newPeerEnabledInConfigProvider; if (callLogFrameworkShouldBeEnabled && !isCallLogFrameworkEnabled) { return Futures.transform( enableFramework(), unused -> { // Reflect the flag changes only after the framework is enabled. sharedPreferences .edit() .putBoolean( NEW_CALL_LOG_FRAGMENT_ENABLED_PREF_KEY, newCallLogFragmentEnabledInConfigProvider) .putBoolean( NEW_VOICEMAIL_FRAGMENT_ENABLED_PREF_KEY, newVoicemailFragmentEnabledInConfigProvider) .putBoolean(NEW_PEER_ENABLED_PREF_KEY, newPeerEnabledInConfigProvider) .putBoolean(NEW_CALL_LOG_FRAMEWORK_ENABLED_PREF_KEY, true) .apply(); return null; }, backgroundExecutor); } else if (!callLogFrameworkShouldBeEnabled && isCallLogFrameworkEnabled) { // Reflect the flag changes before disabling the framework. ListenableFuture writeSharedPrefsFuture = backgroundExecutor.submit( () -> { sharedPreferences .edit() .putBoolean(NEW_CALL_LOG_FRAGMENT_ENABLED_PREF_KEY, false) .putBoolean(NEW_VOICEMAIL_FRAGMENT_ENABLED_PREF_KEY, false) .putBoolean(NEW_PEER_ENABLED_PREF_KEY, false) .putBoolean(NEW_CALL_LOG_FRAMEWORK_ENABLED_PREF_KEY, false) .apply(); return null; }); return Futures.transformAsync( writeSharedPrefsFuture, unused -> disableFramework(), MoreExecutors.directExecutor()); } else { // We didn't need to enable/disable the framework, but we still need to update the // individual flags. return backgroundExecutor.submit( () -> { sharedPreferences .edit() .putBoolean( NEW_CALL_LOG_FRAGMENT_ENABLED_PREF_KEY, newCallLogFragmentEnabledInConfigProvider) .putBoolean( NEW_VOICEMAIL_FRAGMENT_ENABLED_PREF_KEY, newVoicemailFragmentEnabledInConfigProvider) .putBoolean(NEW_PEER_ENABLED_PREF_KEY, newPeerEnabledInConfigProvider) .apply(); return null; }); } } private ListenableFuture enableFramework() { ListenableFuture registerObserversFuture = lightweightExecutor.submit( () -> { callLogFramework.registerContentObservers(); return null; }); ListenableFuture migratorFuture = annotatedCallLogMigrator.migrate(); return Futures.transform( Futures.allAsList(registerObserversFuture, migratorFuture), unused -> null, MoreExecutors.directExecutor()); } private ListenableFuture disableFramework() { return Futures.transform( Futures.allAsList(callLogFramework.disable(), annotatedCallLogMigrator.clearData()), unused -> null, MoreExecutors.directExecutor()); } public boolean isNewCallLogFragmentEnabled() { return sharedPreferences.getBoolean(NEW_CALL_LOG_FRAGMENT_ENABLED_PREF_KEY, false); } public boolean isNewVoicemailFragmentEnabled() { return sharedPreferences.getBoolean(NEW_VOICEMAIL_FRAGMENT_ENABLED_PREF_KEY, false); } public boolean isNewPeerEnabled() { return sharedPreferences.getBoolean(NEW_PEER_ENABLED_PREF_KEY, false); } /** * Returns true if the new call log framework is enabled, meaning that content observers are * firing and PhoneLookupHistory is being populated, etc. */ public boolean isCallLogFrameworkEnabled() { return sharedPreferences.getBoolean(NEW_CALL_LOG_FRAMEWORK_ENABLED_PREF_KEY, false); } static void schedulePollingJob(Context appContext) { if (UserManagerCompat.isUserUnlocked(appContext)) { JobScheduler jobScheduler = Assert.isNotNull(appContext.getSystemService(JobScheduler.class)); @SuppressLint("MissingPermission") // Dialer has RECEIVE_BOOT permission JobInfo jobInfo = new JobInfo.Builder( ScheduledJobIds.CALL_LOG_CONFIG_POLLING_JOB, new ComponentName(appContext, PollingJob.class)) .setPeriodic(TimeUnit.HOURS.toMillis(24)) .setPersisted(true) .setRequiresCharging(true) .setRequiresDeviceIdle(true) .build(); LogUtil.i("CallLogConfig.schedulePollingJob", "scheduling"); jobScheduler.schedule(jobInfo); } } /** * Job which periodically force updates the {@link CallLogConfig}. This job is necessary to * support {@link ConfigProvider ConfigProviders} which do not provide a reliable mechanism for * listening to changes and calling {@link CallLogConfig#update()} directly, such as the {@link * com.android.dialer.configprovider.SharedPrefConfigProvider}. */ public static final class PollingJob extends JobService { @Override public boolean onStartJob(JobParameters params) { LogUtil.enterBlock("PollingJob.onStartJob"); Futures.addCallback( CallLogComponent.get(getApplicationContext()).callLogConfig().update(), new FutureCallback() { @Override public void onSuccess(Void unused) { jobFinished(params, false /* needsReschedule */); } @Override public void onFailure(Throwable throwable) { ThreadUtil.getUiThreadHandler() .post( () -> { throw new RuntimeException(throwable); }); jobFinished(params, false /* needsReschedule */); } }, MoreExecutors.directExecutor()); return true; } @Override public boolean onStopJob(JobParameters params) { return false; } } }