1af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann/*
2af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann * Copyright (C) 2010 The Android Open Source Project
3af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann *
4af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann * Licensed under the Apache License, Version 2.0 (the "License");
5af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann * you may not use this file except in compliance with the License.
6af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann * You may obtain a copy of the License at
7af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann *
8af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann *      http://www.apache.org/licenses/LICENSE-2.0
9af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann *
10af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann * Unless required by applicable law or agreed to in writing, software
11af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann * distributed under the License is distributed on an "AS IS" BASIS,
12af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann * See the License for the specific language governing permissions and
14af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann * limitations under the License.
15af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann */
16af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann
17af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmannpackage com.android.contacts.quickcontact;
18af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann
1990921b3bfd9b3492a19a5fbbf6e0309b97a32425Daniel Lehmannimport android.content.BroadcastReceiver;
208359d04d075243442e973d2c3fdab8d47eb4cbb0Dmitri Plotnikovimport android.content.Context;
21af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmannimport android.content.Intent;
22af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmannimport android.content.IntentFilter;
23af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmannimport android.content.pm.ApplicationInfo;
24af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmannimport android.content.pm.PackageManager;
25af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmannimport android.content.pm.ResolveInfo;
26af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmannimport android.graphics.drawable.Drawable;
278359d04d075243442e973d2c3fdab8d47eb4cbb0Dmitri Plotnikovimport android.provider.ContactsContract.CommonDataKinds.SipAddress;
28af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann
29e0b2f1e2d01d1ac52ba207dc7ce76971d853298eChiao Chengimport com.android.contacts.util.PhoneCapabilityTester;
300a49afa2ad697307cc04ef4cb86570574fa720f2Gary Mai
31e0b2f1e2d01d1ac52ba207dc7ce76971d853298eChiao Chengimport com.google.common.collect.Sets;
32e0b2f1e2d01d1ac52ba207dc7ce76971d853298eChiao Cheng
33af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmannimport java.lang.ref.SoftReference;
34af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmannimport java.util.HashMap;
35af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmannimport java.util.HashSet;
36af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmannimport java.util.List;
37af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann
38af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann/**
39af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann * Internally hold a cache of scaled icons based on {@link PackageManager}
40af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann * queries, keyed internally on MIME-type.
41af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann */
42af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmannpublic class ResolveCache {
43af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann    /**
44af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann     * Specific list {@link ApplicationInfo#packageName} of apps that are
45af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann     * prefered <strong>only</strong> for the purposes of default icons when
46af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann     * multiple {@link ResolveInfo} are found to match. This only happens when
47af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann     * the user has not selected a default app yet, and they will still be
48af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann     * presented with the system disambiguation dialog.
4998103e115089f7339d103276dccb4984ea89fc84Daniel Lehmann     * If several of this list match (e.g. Android Browser vs. Chrome), we will pick either one
50af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann     */
51af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann    private static final HashSet<String> sPreferResolve = Sets.newHashSet(
52af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann            "com.android.email",
5398103e115089f7339d103276dccb4984ea89fc84Daniel Lehmann            "com.google.android.email",
5498103e115089f7339d103276dccb4984ea89fc84Daniel Lehmann
55af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann            "com.android.phone",
5698103e115089f7339d103276dccb4984ea89fc84Daniel Lehmann
5798103e115089f7339d103276dccb4984ea89fc84Daniel Lehmann            "com.google.android.apps.maps",
5898103e115089f7339d103276dccb4984ea89fc84Daniel Lehmann
5998103e115089f7339d103276dccb4984ea89fc84Daniel Lehmann            "com.android.chrome",
604e3c73fded87101d1bf809d307eff5806e254829Tingting Wang            "org.chromium.webview_shell",
6198103e115089f7339d103276dccb4984ea89fc84Daniel Lehmann            "com.google.android.browser",
62af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann            "com.android.browser");
63af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann
648359d04d075243442e973d2c3fdab8d47eb4cbb0Dmitri Plotnikov    private final Context mContext;
65af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann    private final PackageManager mPackageManager;
66af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann
67af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann    private static ResolveCache sInstance;
68af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann
69af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann    /**
70af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann     * Returns an instance of the ResolveCache. Only one internal instance is kept, so
71af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann     * the argument packageManagers is ignored for all but the first call
72af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann     */
738359d04d075243442e973d2c3fdab8d47eb4cbb0Dmitri Plotnikov    public synchronized static ResolveCache getInstance(Context context) {
74af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann        if (sInstance == null) {
7590921b3bfd9b3492a19a5fbbf6e0309b97a32425Daniel Lehmann            final Context applicationContext = context.getApplicationContext();
7690921b3bfd9b3492a19a5fbbf6e0309b97a32425Daniel Lehmann            sInstance = new ResolveCache(applicationContext);
7790921b3bfd9b3492a19a5fbbf6e0309b97a32425Daniel Lehmann
7890921b3bfd9b3492a19a5fbbf6e0309b97a32425Daniel Lehmann            // Register for package-changes so that we can flush our cache
7990921b3bfd9b3492a19a5fbbf6e0309b97a32425Daniel Lehmann            final IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED);
8090921b3bfd9b3492a19a5fbbf6e0309b97a32425Daniel Lehmann            filter.addAction(Intent.ACTION_PACKAGE_REPLACED);
8190921b3bfd9b3492a19a5fbbf6e0309b97a32425Daniel Lehmann            filter.addAction(Intent.ACTION_PACKAGE_REMOVED);
8290921b3bfd9b3492a19a5fbbf6e0309b97a32425Daniel Lehmann            filter.addAction(Intent.ACTION_PACKAGE_CHANGED);
8390921b3bfd9b3492a19a5fbbf6e0309b97a32425Daniel Lehmann            filter.addDataScheme("package");
8490921b3bfd9b3492a19a5fbbf6e0309b97a32425Daniel Lehmann            applicationContext.registerReceiver(sInstance.mPackageIntentReceiver, filter);
85af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann        }
86af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann        return sInstance;
87af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann    }
88af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann
8990921b3bfd9b3492a19a5fbbf6e0309b97a32425Daniel Lehmann    private synchronized static void flush() {
90af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann        sInstance = null;
91af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann    }
92af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann
93af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann    /**
9490921b3bfd9b3492a19a5fbbf6e0309b97a32425Daniel Lehmann     * Called anytime a package is installed, uninstalled etc, so that we can wipe our cache
9590921b3bfd9b3492a19a5fbbf6e0309b97a32425Daniel Lehmann     */
9690921b3bfd9b3492a19a5fbbf6e0309b97a32425Daniel Lehmann    private BroadcastReceiver mPackageIntentReceiver = new BroadcastReceiver() {
9790921b3bfd9b3492a19a5fbbf6e0309b97a32425Daniel Lehmann        @Override
9890921b3bfd9b3492a19a5fbbf6e0309b97a32425Daniel Lehmann        public void onReceive(Context context, Intent intent) {
9990921b3bfd9b3492a19a5fbbf6e0309b97a32425Daniel Lehmann            flush();
10090921b3bfd9b3492a19a5fbbf6e0309b97a32425Daniel Lehmann        }
10190921b3bfd9b3492a19a5fbbf6e0309b97a32425Daniel Lehmann    };
10290921b3bfd9b3492a19a5fbbf6e0309b97a32425Daniel Lehmann
10390921b3bfd9b3492a19a5fbbf6e0309b97a32425Daniel Lehmann    /**
104af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann     * Cached entry holding the best {@link ResolveInfo} for a specific
105af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann     * MIME-type, along with a {@link SoftReference} to its icon.
106af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann     */
107af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann    private static class Entry {
108af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann        public ResolveInfo bestResolve;
109af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann        public Drawable icon;
110af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann    }
111af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann
112af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann    private HashMap<String, Entry> mCache = new HashMap<String, Entry>();
113af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann
1148359d04d075243442e973d2c3fdab8d47eb4cbb0Dmitri Plotnikov
1158359d04d075243442e973d2c3fdab8d47eb4cbb0Dmitri Plotnikov    private ResolveCache(Context context) {
1168359d04d075243442e973d2c3fdab8d47eb4cbb0Dmitri Plotnikov        mContext = context;
1178359d04d075243442e973d2c3fdab8d47eb4cbb0Dmitri Plotnikov        mPackageManager = context.getPackageManager();
118af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann    }
119af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann
120af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann    /**
121eb64a4b5c51b39fe56ba4ef97dfff73fdcdf8c75Paul Soulos     * Get the {@link Entry} best associated with the given mimetype and intent,
122af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann     * or create and populate a new one if it doesn't exist.
123af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann     */
124eb64a4b5c51b39fe56ba4ef97dfff73fdcdf8c75Paul Soulos    protected Entry getEntry(String mimeType, Intent intent) {
125af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann        Entry entry = mCache.get(mimeType);
126af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann        if (entry != null) return entry;
127af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann        entry = new Entry();
128af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann
1298359d04d075243442e973d2c3fdab8d47eb4cbb0Dmitri Plotnikov        if (SipAddress.CONTENT_ITEM_TYPE.equals(mimeType)
1308359d04d075243442e973d2c3fdab8d47eb4cbb0Dmitri Plotnikov                && !PhoneCapabilityTester.isSipPhone(mContext)) {
1318359d04d075243442e973d2c3fdab8d47eb4cbb0Dmitri Plotnikov            intent = null;
1328359d04d075243442e973d2c3fdab8d47eb4cbb0Dmitri Plotnikov        }
1338359d04d075243442e973d2c3fdab8d47eb4cbb0Dmitri Plotnikov
134af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann        if (intent != null) {
135af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann            final List<ResolveInfo> matches = mPackageManager.queryIntentActivities(intent,
136af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann                    PackageManager.MATCH_DEFAULT_ONLY);
137af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann
138af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann            // Pick first match, otherwise best found
139af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann            ResolveInfo bestResolve = null;
140af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann            final int size = matches.size();
141af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann            if (size == 1) {
142af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann                bestResolve = matches.get(0);
143af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann            } else if (size > 1) {
144af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann                bestResolve = getBestResolve(intent, matches);
145af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann            }
146af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann
147af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann            if (bestResolve != null) {
148af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann                final Drawable icon = bestResolve.loadIcon(mPackageManager);
149af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann
150af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann                entry.bestResolve = bestResolve;
151af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann                entry.icon = icon;
152af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann            }
153af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann        }
154af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann
155af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann        mCache.put(mimeType, entry);
156af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann        return entry;
157af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann    }
158af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann
159af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann    /**
160af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann     * Best {@link ResolveInfo} when multiple found. Ties are broken by
161edb576aab33efff623691a89ace3c76cb2ff12d1Daniel Lehmann     * selecting first from the {@link QuickContactActivity#sPreferResolve} list of
162af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann     * preferred packages, second by apps that live on the system partition,
163af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann     * otherwise the app from the top of the list. This is
164af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann     * <strong>only</strong> used for selecting a default icon for
165af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann     * displaying in the track, and does not shortcut the system
166af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann     * {@link Intent} disambiguation dialog.
167af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann     */
168af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann    protected ResolveInfo getBestResolve(Intent intent, List<ResolveInfo> matches) {
169af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann        // Try finding preferred activity, otherwise detect disambig
170af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann        final ResolveInfo foundResolve = mPackageManager.resolveActivity(intent,
171af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann                PackageManager.MATCH_DEFAULT_ONLY);
172af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann        final boolean foundDisambig = (foundResolve.match &
173af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann                IntentFilter.MATCH_CATEGORY_MASK) == 0;
174af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann
175af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann        if (!foundDisambig) {
176af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann            // Found concrete match, so return directly
177af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann            return foundResolve;
178af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann        }
179af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann
180af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann        // Accept any package from prefer list, otherwise first system app
181af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann        ResolveInfo firstSystem = null;
182af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann        for (ResolveInfo info : matches) {
183af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann            final boolean isSystem = (info.activityInfo.applicationInfo.flags
184af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann                    & ApplicationInfo.FLAG_SYSTEM) != 0;
185af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann            final boolean isPrefer = sPreferResolve
186af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann                    .contains(info.activityInfo.applicationInfo.packageName);
187af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann
188af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann            if (isPrefer) return info;
189af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann            if (isSystem && firstSystem == null) firstSystem = info;
190af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann        }
191af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann
192af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann        // Return first system found, otherwise first from list
193af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann        return firstSystem != null ? firstSystem : matches.get(0);
194af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann    }
195af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann
196af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann    /**
197af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann     * Check {@link PackageManager} to see if any apps offer to handle the
198eb64a4b5c51b39fe56ba4ef97dfff73fdcdf8c75Paul Soulos     * given {@link Intent}.
199af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann     */
200eb64a4b5c51b39fe56ba4ef97dfff73fdcdf8c75Paul Soulos    public boolean hasResolve(String mimeType, Intent intent) {
201eb64a4b5c51b39fe56ba4ef97dfff73fdcdf8c75Paul Soulos        return getEntry(mimeType, intent).bestResolve != null;
202af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann    }
203af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann
204af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann    /**
205af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann     * Return the best icon for the given {@link Action}, which is usually
206af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann     * based on the {@link ResolveInfo} found through a
207af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann     * {@link PackageManager} query.
208af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann     */
209eb64a4b5c51b39fe56ba4ef97dfff73fdcdf8c75Paul Soulos    public Drawable getIcon(String mimeType, Intent intent) {
210eb64a4b5c51b39fe56ba4ef97dfff73fdcdf8c75Paul Soulos        return getEntry(mimeType, intent).icon;
211af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann    }
212af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann
213af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann    public void clear() {
214af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann        mCache.clear();
215af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann    }
216af8e3864a2d0131f72337165c846fe909a099e52Daniel Lehmann}
217