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 
17 package com.android.dialer.calllog;
18 
19 import android.annotation.TargetApi;
20 import android.content.Context;
21 import android.content.SharedPreferences;
22 import android.database.sqlite.SQLiteDatabase;
23 import android.os.Build;
24 import android.preference.PreferenceManager;
25 import android.support.annotation.WorkerThread;
26 import com.android.dialer.calllog.database.AnnotatedCallLog;
27 import com.android.dialer.calllog.database.CallLogMutations;
28 import com.android.dialer.calllog.datasources.CallLogDataSource;
29 import com.android.dialer.common.Assert;
30 import com.android.dialer.common.LogUtil;
31 import com.android.dialer.common.concurrent.DialerExecutor.Worker;
32 import javax.inject.Inject;
33 
34 /**
35  * Worker which brings the annotated call log up to date, if necessary.
36  *
37  * <p>Accepts a boolean which indicates if the dirty check should be skipped, and returns true if
38  * the annotated call log was updated.
39  */
40 public class RefreshAnnotatedCallLogWorker implements Worker<Boolean, Boolean> {
41 
42   private final Context appContext;
43   private final DataSources dataSources;
44 
45   @Inject
RefreshAnnotatedCallLogWorker(Context appContext, DataSources dataSources)46   public RefreshAnnotatedCallLogWorker(Context appContext, DataSources dataSources) {
47     this.appContext = appContext;
48     this.dataSources = dataSources;
49   }
50 
51   @Override
doInBackground(Boolean skipDirtyCheck)52   public Boolean doInBackground(Boolean skipDirtyCheck) {
53     LogUtil.enterBlock("RefreshAnnotatedCallLogWorker.doInBackgroundFallible");
54 
55     long startTime = System.currentTimeMillis();
56     boolean annotatedCallLogUpdated = checkDirtyAndRebuildIfNecessary(appContext, skipDirtyCheck);
57     LogUtil.i(
58         "RefreshAnnotatedCallLogWorker.doInBackgroundFallible",
59         "updated? %s, took %dms",
60         annotatedCallLogUpdated,
61         System.currentTimeMillis() - startTime);
62     return annotatedCallLogUpdated;
63   }
64 
65   @WorkerThread
checkDirtyAndRebuildIfNecessary(Context appContext, boolean skipDirtyCheck)66   private boolean checkDirtyAndRebuildIfNecessary(Context appContext, boolean skipDirtyCheck) {
67     Assert.isWorkerThread();
68 
69     long startTime = System.currentTimeMillis();
70 
71     SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(appContext);
72     long lastRebuildTimeMillis =
73         sharedPreferences.getLong(CallLogFramework.PREF_LAST_REBUILD_TIMESTAMP_MILLIS, 0);
74     if (lastRebuildTimeMillis == 0) {
75       LogUtil.i(
76           "RefreshAnnotatedCallLogWorker.checkDirtyAndRebuildIfNecessary",
77           "annotated call log has never been built, marking it dirty");
78     }
79     boolean forceRebuildPrefValue =
80         sharedPreferences.getBoolean(CallLogFramework.PREF_FORCE_REBUILD, false);
81     if (forceRebuildPrefValue) {
82       LogUtil.i(
83           "RefreshAnnotatedCallLogWorker.checkDirtyAndRebuildIfNecessary",
84           "call log has been marked dirty");
85     }
86 
87     boolean isDirty =
88         lastRebuildTimeMillis == 0
89             || skipDirtyCheck
90             || forceRebuildPrefValue
91             || isDirty(appContext);
92     LogUtil.i(
93         "RefreshAnnotatedCallLogWorker.checkDirtyAndRebuildIfNecessary",
94         "isDirty took: %dms",
95         System.currentTimeMillis() - startTime);
96     if (isDirty) {
97       startTime = System.currentTimeMillis();
98       rebuild(appContext, lastRebuildTimeMillis);
99       LogUtil.i(
100           "RefreshAnnotatedCallLogWorker.checkDirtyAndRebuildIfNecessary",
101           "rebuild took: %dms",
102           System.currentTimeMillis() - startTime);
103       return true; // Annotated call log was updated.
104     }
105     return false; // Annotated call log was not updated.
106   }
107 
108   @WorkerThread
isDirty(Context appContext)109   private boolean isDirty(Context appContext) {
110     Assert.isWorkerThread();
111 
112     for (CallLogDataSource dataSource : dataSources.getDataSourcesIncludingSystemCallLog()) {
113       String dataSourceName = getName(dataSource);
114       long startTime = System.currentTimeMillis();
115       LogUtil.i("RefreshAnnotatedCallLogWorker.isDirty", "running isDirty for %s", dataSourceName);
116       boolean isDirty = dataSource.isDirty(appContext);
117       LogUtil.i(
118           "RefreshAnnotatedCallLogWorker.isDirty",
119           "%s.isDirty returned %b in %dms",
120           dataSourceName,
121           isDirty,
122           System.currentTimeMillis() - startTime);
123       if (isDirty) {
124         return true;
125       }
126     }
127     return false;
128   }
129 
130   @TargetApi(Build.VERSION_CODES.M) // Uses try-with-resources
131   @WorkerThread
rebuild(Context appContext, long lastRebuildTimeMillis)132   private void rebuild(Context appContext, long lastRebuildTimeMillis) {
133     Assert.isWorkerThread();
134 
135     // TODO: Start a transaction?
136     try (SQLiteDatabase database = AnnotatedCallLog.getWritableDatabase(appContext)) {
137 
138       CallLogMutations mutations = new CallLogMutations();
139 
140       // System call log data source must go first!
141       CallLogDataSource systemCallLogDataSource = dataSources.getSystemCallLogDataSource();
142       String dataSourceName = getName(systemCallLogDataSource);
143       LogUtil.i("RefreshAnnotatedCallLogWorker.rebuild", "filling %s", dataSourceName);
144       long startTime = System.currentTimeMillis();
145       systemCallLogDataSource.fill(appContext, database, lastRebuildTimeMillis, mutations);
146       LogUtil.i(
147           "RefreshAnnotatedCallLogWorker.rebuild",
148           "%s.fill took: %dms",
149           dataSourceName,
150           System.currentTimeMillis() - startTime);
151 
152       for (CallLogDataSource dataSource : dataSources.getDataSourcesExcludingSystemCallLog()) {
153         dataSourceName = getName(dataSource);
154         LogUtil.i("RefreshAnnotatedCallLogWorker.rebuild", "filling %s", dataSourceName);
155         startTime = System.currentTimeMillis();
156         dataSource.fill(appContext, database, lastRebuildTimeMillis, mutations);
157         LogUtil.i(
158             "CallLogFramework.rebuild",
159             "%s.fill took: %dms",
160             dataSourceName,
161             System.currentTimeMillis() - startTime);
162       }
163       LogUtil.i("RefreshAnnotatedCallLogWorker.rebuild", "applying mutations to database");
164       startTime = System.currentTimeMillis();
165       mutations.applyToDatabase(database);
166       LogUtil.i(
167           "RefreshAnnotatedCallLogWorker.rebuild",
168           "applyToDatabase took: %dms",
169           System.currentTimeMillis() - startTime);
170     }
171 
172     SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(appContext);
173     sharedPreferences
174         .edit()
175         .putBoolean(CallLogFramework.PREF_FORCE_REBUILD, false)
176         .putLong(CallLogFramework.PREF_LAST_REBUILD_TIMESTAMP_MILLIS, System.currentTimeMillis())
177         .commit();
178   }
179 
getName(CallLogDataSource dataSource)180   private static String getName(CallLogDataSource dataSource) {
181     return dataSource.getClass().getSimpleName();
182   }
183 }
184