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 */
16package com.android.providers.contacts;
17
18import android.content.BroadcastReceiver;
19import android.content.BroadcastReceiver.PendingResult;
20import android.content.ContentProvider;
21import android.content.Context;
22import android.content.IContentProvider;
23import android.content.Intent;
24import android.content.IntentFilter;
25import android.provider.ContactsContract;
26import android.provider.VoicemailContract;
27import android.text.TextUtils;
28import android.util.Log;
29import android.util.Slog;
30
31import com.android.providers.contacts.util.PackageUtils;
32
33import com.google.common.annotations.VisibleForTesting;
34
35/**
36 * - Handles package related broadcasts.
37 * - Also scan changed packages while the process wasn't running using PM.getChangedPackages().
38 */
39public class ContactsPackageMonitor {
40    private static final String TAG = "ContactsPackageMonitor";
41
42    private static final boolean VERBOSE_LOGGING = AbstractContactsProvider.VERBOSE_LOGGING;
43
44    private static final int BACKGROUND_TASK_PACKAGE_EVENT = 0;
45
46    private static ContactsPackageMonitor sInstance;
47
48    private Context mContext;
49
50    /** We run all BG tasks on this thread/handler sequentially. */
51    private final ContactsTaskScheduler mTaskScheduler;
52
53    private static class PackageEventArg {
54        final String packageName;
55        final PendingResult broadcastPendingResult;
56
57        private PackageEventArg(String packageName, PendingResult broadcastPendingResult) {
58            this.packageName = packageName;
59            this.broadcastPendingResult = broadcastPendingResult;
60        }
61    }
62
63    private ContactsPackageMonitor(Context context) {
64        mContext = context; // Can't use the app context due to a bug with shared process.
65
66        // Start the BG thread and register the receiver.
67        mTaskScheduler = new ContactsTaskScheduler(getClass().getSimpleName()) {
68            @Override
69            public void onPerformTask(int taskId, Object arg) {
70                switch (taskId) {
71                    case BACKGROUND_TASK_PACKAGE_EVENT:
72                        onPackageChanged((PackageEventArg) arg);
73                        break;
74                }
75            }
76        };
77    }
78
79    private void start() {
80        if (VERBOSE_LOGGING) {
81            Log.v(TAG, "Starting... user="
82                    + android.os.Process.myUserHandle().getIdentifier());
83        }
84
85        registerReceiver();
86    }
87
88    public static synchronized void start(Context context) {
89        if (sInstance == null) {
90            sInstance = new ContactsPackageMonitor(context);
91            sInstance.start();
92        }
93    }
94
95    private void registerReceiver() {
96        final IntentFilter filter = new IntentFilter();
97
98        filter.addAction(Intent.ACTION_PACKAGE_ADDED);
99        filter.addAction(Intent.ACTION_PACKAGE_REMOVED);
100        filter.addAction(Intent.ACTION_PACKAGE_REPLACED);
101        filter.addAction(Intent.ACTION_PACKAGE_CHANGED);
102        filter.addDataScheme("package");
103
104        mContext.registerReceiver(new BroadcastReceiver() {
105            @Override
106            public void onReceive(Context context, Intent intent) {
107                if (intent.getData() == null) {
108                    return; // Shouldn't happen.
109                }
110                final String changedPackage = intent.getData().getSchemeSpecificPart();
111                final PendingResult result = goAsync();
112
113                mTaskScheduler.scheduleTask(BACKGROUND_TASK_PACKAGE_EVENT,
114                        new PackageEventArg(changedPackage, result));
115            }
116        }, filter);
117    }
118
119    private void onPackageChanged(PackageEventArg arg) {
120        try {
121            final String packageName = arg.packageName;
122            if (TextUtils.isEmpty(packageName)) {
123                Log.w(TAG, "Empty package name detected.");
124                return;
125            }
126            if (VERBOSE_LOGGING) Log.d(TAG, "onPackageChanged: Scanning package: " + packageName);
127
128            // First, tell CP2.
129            final ContactsProvider2 provider = getProvider(mContext, ContactsContract.AUTHORITY);
130            if (provider != null) {
131                provider.onPackageChanged(packageName);
132            }
133
134            // Next, if the package is gone, clean up the voicemail.
135            cleanupVoicemail(mContext, packageName);
136        } finally {
137            if (VERBOSE_LOGGING) Log.v(TAG, "Calling PendingResult.finish()...");
138            arg.broadcastPendingResult.finish();
139        }
140    }
141
142    @VisibleForTesting
143    static void cleanupVoicemail(Context context, String packageName) {
144        if (PackageUtils.isPackageInstalled(context, packageName)) {
145            return; // Still installed.
146        }
147        if (VERBOSE_LOGGING) Log.d(TAG, "Cleaning up data for package: " + packageName);
148
149        // Delete both voicemail content and voicemail status entries for this package.
150        final VoicemailContentProvider provider = getProvider(context, VoicemailContract.AUTHORITY);
151        if (provider != null) {
152            provider.removeBySourcePackage(packageName);
153        }
154    }
155
156    private static <T extends ContentProvider> T getProvider(Context context, String authority) {
157        final IContentProvider iprovider = context.getContentResolver().acquireProvider(authority);
158        final ContentProvider provider = ContentProvider.coerceToLocalContentProvider(iprovider);
159        if (provider != null) {
160            return (T) provider;
161        }
162        Slog.wtf(TAG, "Provider for " + authority + " not found");
163        return null;
164    }
165}
166