1 /*
2  * Copyright (C) 2018 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.config;
18 
19 import android.annotation.SuppressLint;
20 import android.app.job.JobInfo;
21 import android.app.job.JobParameters;
22 import android.app.job.JobScheduler;
23 import android.app.job.JobService;
24 import android.content.ComponentName;
25 import android.content.Context;
26 import android.content.SharedPreferences;
27 import android.support.v4.os.UserManagerCompat;
28 import com.android.dialer.calllog.CallLogFramework;
29 import com.android.dialer.common.Assert;
30 import com.android.dialer.common.LogUtil;
31 import com.android.dialer.common.concurrent.Annotations.BackgroundExecutor;
32 import com.android.dialer.common.concurrent.ThreadUtil;
33 import com.android.dialer.configprovider.ConfigProvider;
34 import com.android.dialer.constants.ScheduledJobIds;
35 import com.android.dialer.inject.ApplicationContext;
36 import com.android.dialer.storage.Unencrypted;
37 import com.google.common.util.concurrent.FutureCallback;
38 import com.google.common.util.concurrent.Futures;
39 import com.google.common.util.concurrent.ListenableFuture;
40 import com.google.common.util.concurrent.ListeningExecutorService;
41 import com.google.common.util.concurrent.MoreExecutors;
42 import java.util.concurrent.TimeUnit;
43 import javax.inject.Inject;
44 
45 /**
46  * Determines if new call log components are enabled.
47  *
48  * <p>When the underlying flag values from the {@link ConfigProvider} changes, it is necessary to do
49  * work such as registering/unregistering content observers, and this class is responsible for
50  * coordinating that work.
51  *
52  * <p>New UI application components should use this class instead of reading flags directly from the
53  * {@link ConfigProvider}.
54  */
55 public final class CallLogConfigImpl implements CallLogConfig {
56 
57   private static final String NEW_CALL_LOG_FRAGMENT_ENABLED_PREF_KEY = "newCallLogFragmentEnabled";
58   private static final String NEW_VOICEMAIL_FRAGMENT_ENABLED_PREF_KEY =
59       "newVoicemailFragmentEnabled";
60   private static final String NEW_PEER_ENABLED_PREF_KEY = "newPeerEnabled";
61   private static final String NEW_CALL_LOG_FRAMEWORK_ENABLED_PREF_KEY =
62       "newCallLogFrameworkEnabled";
63 
64   private final Context appContext;
65   private final CallLogFramework callLogFramework;
66   private final SharedPreferences sharedPreferences;
67   private final ConfigProvider configProvider;
68   private final ListeningExecutorService backgroundExecutor;
69 
70   @Inject
CallLogConfigImpl( @pplicationContext Context appContext, CallLogFramework callLogFramework, @Unencrypted SharedPreferences sharedPreferences, ConfigProvider configProvider, @BackgroundExecutor ListeningExecutorService backgroundExecutor)71   public CallLogConfigImpl(
72       @ApplicationContext Context appContext,
73       CallLogFramework callLogFramework,
74       @Unencrypted SharedPreferences sharedPreferences,
75       ConfigProvider configProvider,
76       @BackgroundExecutor ListeningExecutorService backgroundExecutor) {
77     this.appContext = appContext;
78     this.callLogFramework = callLogFramework;
79     this.sharedPreferences = sharedPreferences;
80     this.configProvider = configProvider;
81     this.backgroundExecutor = backgroundExecutor;
82   }
83 
84   @Override
update()85   public ListenableFuture<Void> update() {
86     boolean newCallLogFragmentEnabledInConfigProvider =
87         configProvider.getBoolean("new_call_log_fragment_enabled", false);
88     boolean newVoicemailFragmentEnabledInConfigProvider =
89         configProvider.getBoolean("new_voicemail_fragment_enabled", false);
90     boolean newPeerEnabledInConfigProvider = configProvider.getBoolean("nui_peer_enabled", false);
91 
92     boolean isCallLogFrameworkEnabled = isCallLogFrameworkEnabled();
93     boolean callLogFrameworkShouldBeEnabled =
94         newCallLogFragmentEnabledInConfigProvider
95             || newVoicemailFragmentEnabledInConfigProvider
96             || newPeerEnabledInConfigProvider;
97 
98     if (callLogFrameworkShouldBeEnabled && !isCallLogFrameworkEnabled) {
99       return Futures.transform(
100           callLogFramework.enable(),
101           unused -> {
102             // Reflect the flag changes only after the framework is enabled.
103             sharedPreferences
104                 .edit()
105                 .putBoolean(
106                     NEW_CALL_LOG_FRAGMENT_ENABLED_PREF_KEY,
107                     newCallLogFragmentEnabledInConfigProvider)
108                 .putBoolean(
109                     NEW_VOICEMAIL_FRAGMENT_ENABLED_PREF_KEY,
110                     newVoicemailFragmentEnabledInConfigProvider)
111                 .putBoolean(NEW_PEER_ENABLED_PREF_KEY, newPeerEnabledInConfigProvider)
112                 .putBoolean(NEW_CALL_LOG_FRAMEWORK_ENABLED_PREF_KEY, true)
113                 .apply();
114             return null;
115           },
116           backgroundExecutor);
117     } else if (!callLogFrameworkShouldBeEnabled && isCallLogFrameworkEnabled) {
118       // Reflect the flag changes before disabling the framework.
119       ListenableFuture<Void> writeSharedPrefsFuture =
120           backgroundExecutor.submit(
121               () -> {
122                 sharedPreferences
123                     .edit()
124                     .putBoolean(NEW_CALL_LOG_FRAGMENT_ENABLED_PREF_KEY, false)
125                     .putBoolean(NEW_VOICEMAIL_FRAGMENT_ENABLED_PREF_KEY, false)
126                     .putBoolean(NEW_PEER_ENABLED_PREF_KEY, false)
127                     .putBoolean(NEW_CALL_LOG_FRAMEWORK_ENABLED_PREF_KEY, false)
128                     .apply();
129                 return null;
130               });
131       return Futures.transformAsync(
132           writeSharedPrefsFuture,
133           unused -> callLogFramework.disable(),
134           MoreExecutors.directExecutor());
135     } else {
136       // We didn't need to enable/disable the framework, but we still need to update the
137       // individual flags.
138       return backgroundExecutor.submit(
139           () -> {
140             sharedPreferences
141                 .edit()
142                 .putBoolean(
143                     NEW_CALL_LOG_FRAGMENT_ENABLED_PREF_KEY,
144                     newCallLogFragmentEnabledInConfigProvider)
145                 .putBoolean(
146                     NEW_VOICEMAIL_FRAGMENT_ENABLED_PREF_KEY,
147                     newVoicemailFragmentEnabledInConfigProvider)
148                 .putBoolean(NEW_PEER_ENABLED_PREF_KEY, newPeerEnabledInConfigProvider)
149                 .apply();
150             return null;
151           });
152     }
153   }
154 
155   @Override
156   public boolean isNewCallLogFragmentEnabled() {
157     return sharedPreferences.getBoolean(NEW_CALL_LOG_FRAGMENT_ENABLED_PREF_KEY, false);
158   }
159 
160   @Override
161   public boolean isNewVoicemailFragmentEnabled() {
162     return sharedPreferences.getBoolean(NEW_VOICEMAIL_FRAGMENT_ENABLED_PREF_KEY, false);
163   }
164 
165   @Override
166   public boolean isNewPeerEnabled() {
167     return sharedPreferences.getBoolean(NEW_PEER_ENABLED_PREF_KEY, false);
168   }
169 
170   /**
171    * Returns true if the new call log framework is enabled, meaning that content observers are
172    * firing and PhoneLookupHistory is being populated, etc.
173    */
174   @Override
175   public boolean isCallLogFrameworkEnabled() {
176     return sharedPreferences.getBoolean(NEW_CALL_LOG_FRAMEWORK_ENABLED_PREF_KEY, false);
177   }
178 
179   @Override
180   public void schedulePollingJob() {
181     if (UserManagerCompat.isUserUnlocked(appContext)) {
182       JobScheduler jobScheduler = Assert.isNotNull(appContext.getSystemService(JobScheduler.class));
183       @SuppressLint("MissingPermission") // Dialer has RECEIVE_BOOT permission
184       JobInfo jobInfo =
185           new JobInfo.Builder(
186                   ScheduledJobIds.CALL_LOG_CONFIG_POLLING_JOB,
187                   new ComponentName(appContext, PollingJob.class))
188               .setPeriodic(TimeUnit.HOURS.toMillis(24))
189               .setPersisted(true)
190               .setRequiresCharging(true)
191               .setRequiresDeviceIdle(true)
192               .build();
193       LogUtil.i("CallLogConfigImpl.schedulePollingJob", "scheduling");
194       jobScheduler.schedule(jobInfo);
195     }
196   }
197 
198   /**
199    * Job which periodically force updates the {@link CallLogConfig}. This job is necessary to
200    * support {@link ConfigProvider ConfigProviders} which do not provide a reliable mechanism for
201    * listening to changes and calling {@link CallLogConfig#update()} directly, such as the {@link
202    * com.android.dialer.configprovider.SharedPrefConfigProvider}.
203    */
204   public static final class PollingJob extends JobService {
205 
206     @Override
207     public boolean onStartJob(JobParameters params) {
208       LogUtil.enterBlock("PollingJob.onStartJob");
209       Futures.addCallback(
210           CallLogConfigComponent.get(getApplicationContext()).callLogConfig().update(),
211           new FutureCallback<Void>() {
212             @Override
213             public void onSuccess(Void unused) {
214               jobFinished(params, false /* needsReschedule */);
215             }
216 
217             @Override
218             public void onFailure(Throwable throwable) {
219               ThreadUtil.getUiThreadHandler()
220                   .post(
221                       () -> {
222                         throw new RuntimeException(throwable);
223                       });
224               jobFinished(params, false /* needsReschedule */);
225             }
226           },
227           MoreExecutors.directExecutor());
228       return true;
229     }
230 
231     @Override
232     public boolean onStopJob(JobParameters params) {
233       return false;
234     }
235   }
236 }
237