1/*
2 * Copyright (C) 2011 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
17package com.android.contacts.detail;
18
19import android.app.Activity;
20import android.app.Fragment;
21import android.app.LoaderManager;
22import android.app.LoaderManager.LoaderCallbacks;
23import android.content.ActivityNotFoundException;
24import android.content.Context;
25import android.content.Intent;
26import android.content.Loader;
27import android.media.RingtoneManager;
28import android.net.Uri;
29import android.os.Bundle;
30import android.provider.ContactsContract;
31import android.provider.ContactsContract.Contacts;
32import android.util.Log;
33import android.view.KeyEvent;
34import android.view.LayoutInflater;
35import android.view.Menu;
36import android.view.MenuInflater;
37import android.view.MenuItem;
38import android.view.View;
39import android.view.ViewGroup;
40import android.widget.Toast;
41
42import com.android.contacts.ContactSaveService;
43import com.android.contacts.R;
44import com.android.contacts.activities.ContactDetailActivity.FragmentKeyListener;
45import com.android.contacts.list.ShortcutIntentBuilder;
46import com.android.contacts.list.ShortcutIntentBuilder.OnShortcutIntentCreatedListener;
47import com.android.contacts.model.Contact;
48import com.android.contacts.model.ContactLoader;
49import com.android.contacts.util.PhoneCapabilityTester;
50import com.android.internal.util.Objects;
51
52/**
53 * This is an invisible worker {@link Fragment} that loads the contact details for the contact card.
54 * The data is then passed to the listener, who can then pass the data to other {@link View}s.
55 */
56public class ContactLoaderFragment extends Fragment implements FragmentKeyListener {
57
58    private static final String TAG = ContactLoaderFragment.class.getSimpleName();
59
60    /** The launch code when picking a ringtone */
61    private static final int REQUEST_CODE_PICK_RINGTONE = 1;
62
63    /** This is the Intent action to install a shortcut in the launcher. */
64    private static final String ACTION_INSTALL_SHORTCUT =
65            "com.android.launcher.action.INSTALL_SHORTCUT";
66
67    private boolean mOptionsMenuOptions;
68    private boolean mOptionsMenuEditable;
69    private boolean mOptionsMenuShareable;
70    private boolean mOptionsMenuCanCreateShortcut;
71    private boolean mSendToVoicemailState;
72    private String mCustomRingtone;
73
74    /**
75     * This is a listener to the {@link ContactLoaderFragment} and will be notified when the
76     * contact details have finished loading or if the user selects any menu options.
77     */
78    public static interface ContactLoaderFragmentListener {
79        /**
80         * Contact was not found, so somehow close this fragment. This is raised after a contact
81         * is removed via Menu/Delete
82         */
83        public void onContactNotFound();
84
85        /**
86         * Contact details have finished loading.
87         */
88        public void onDetailsLoaded(Contact result);
89
90        /**
91         * User decided to go to Edit-Mode
92         */
93        public void onEditRequested(Uri lookupUri);
94
95        /**
96         * User decided to delete the contact
97         */
98        public void onDeleteRequested(Uri lookupUri);
99
100    }
101
102    private static final int LOADER_DETAILS = 1;
103
104    private static final String KEY_CONTACT_URI = "contactUri";
105    private static final String LOADER_ARG_CONTACT_URI = "contactUri";
106
107    private Context mContext;
108    private Uri mLookupUri;
109    private ContactLoaderFragmentListener mListener;
110
111    private Contact mContactData;
112
113    public ContactLoaderFragment() {
114    }
115
116    @Override
117    public void onCreate(Bundle savedInstanceState) {
118        super.onCreate(savedInstanceState);
119        if (savedInstanceState != null) {
120            mLookupUri = savedInstanceState.getParcelable(KEY_CONTACT_URI);
121        }
122    }
123
124    @Override
125    public void onSaveInstanceState(Bundle outState) {
126        super.onSaveInstanceState(outState);
127        outState.putParcelable(KEY_CONTACT_URI, mLookupUri);
128    }
129
130    @Override
131    public void onAttach(Activity activity) {
132        super.onAttach(activity);
133        mContext = activity;
134    }
135
136    @Override
137    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) {
138        setHasOptionsMenu(true);
139        // This is an invisible view.  This fragment is declared in a layout, so it can't be
140        // "viewless".  (i.e. can't return null here.)
141        // See also the comment in the layout file.
142        return inflater.inflate(R.layout.contact_detail_loader_fragment, container, false);
143    }
144
145    @Override
146    public void onActivityCreated(Bundle savedInstanceState) {
147        super.onActivityCreated(savedInstanceState);
148
149        if (mLookupUri != null) {
150            Bundle args = new Bundle();
151            args.putParcelable(LOADER_ARG_CONTACT_URI, mLookupUri);
152            getLoaderManager().initLoader(LOADER_DETAILS, args, mDetailLoaderListener);
153        }
154    }
155
156    public void loadUri(Uri lookupUri) {
157        if (Objects.equal(lookupUri, mLookupUri)) {
158            // Same URI, no need to load the data again
159            return;
160        }
161
162        mLookupUri = lookupUri;
163        if (mLookupUri == null) {
164            getLoaderManager().destroyLoader(LOADER_DETAILS);
165            mContactData = null;
166            if (mListener != null) {
167                mListener.onDetailsLoaded(mContactData);
168            }
169        } else if (getActivity() != null) {
170            Bundle args = new Bundle();
171            args.putParcelable(LOADER_ARG_CONTACT_URI, mLookupUri);
172            getLoaderManager().restartLoader(LOADER_DETAILS, args, mDetailLoaderListener);
173        }
174    }
175
176    public void setListener(ContactLoaderFragmentListener value) {
177        mListener = value;
178    }
179
180    /**
181     * The listener for the detail loader
182     */
183    private final LoaderManager.LoaderCallbacks<Contact> mDetailLoaderListener =
184            new LoaderCallbacks<Contact>() {
185        @Override
186        public Loader<Contact> onCreateLoader(int id, Bundle args) {
187            Uri lookupUri = args.getParcelable(LOADER_ARG_CONTACT_URI);
188            return new ContactLoader(mContext, lookupUri, true /* loadGroupMetaData */,
189                    true /* loadStreamItems */, true /* load invitable account types */,
190                    true /* postViewNotification */, true /* computeFormattedPhoneNumber */);
191        }
192
193        @Override
194        public void onLoadFinished(Loader<Contact> loader, Contact data) {
195            if (!mLookupUri.equals(data.getRequestedUri())) {
196                Log.e(TAG, "Different URI: requested=" + mLookupUri + "  actual=" + data);
197                return;
198            }
199
200            if (data.isError()) {
201                // This shouldn't ever happen, so throw an exception. The {@link ContactLoader}
202                // should log the actual exception.
203                throw new IllegalStateException("Failed to load contact", data.getException());
204            } else if (data.isNotFound()) {
205                Log.i(TAG, "No contact found: " + ((ContactLoader)loader).getLookupUri());
206                mContactData = null;
207            } else {
208                mContactData = data;
209            }
210
211            if (mListener != null) {
212                if (mContactData == null) {
213                    mListener.onContactNotFound();
214                } else {
215                    mListener.onDetailsLoaded(mContactData);
216                }
217            }
218            // Make sure the options menu is setup correctly with the loaded data.
219            if (getActivity() != null) getActivity().invalidateOptionsMenu();
220        }
221
222        @Override
223        public void onLoaderReset(Loader<Contact> loader) {}
224    };
225
226    @Override
227    public void onCreateOptionsMenu(Menu menu, final MenuInflater inflater) {
228        inflater.inflate(R.menu.view_contact, menu);
229    }
230
231    public boolean isOptionsMenuChanged() {
232        return mOptionsMenuOptions != isContactOptionsChangeEnabled()
233                || mOptionsMenuEditable != isContactEditable()
234                || mOptionsMenuShareable != isContactShareable()
235                || mOptionsMenuCanCreateShortcut != isContactCanCreateShortcut();
236    }
237
238    @Override
239    public void onPrepareOptionsMenu(Menu menu) {
240        mOptionsMenuOptions = isContactOptionsChangeEnabled();
241        mOptionsMenuEditable = isContactEditable();
242        mOptionsMenuShareable = isContactShareable();
243        mOptionsMenuCanCreateShortcut = isContactCanCreateShortcut();
244        if (mContactData != null) {
245            mSendToVoicemailState = mContactData.isSendToVoicemail();
246            mCustomRingtone = mContactData.getCustomRingtone();
247        }
248
249        // Hide telephony-related settings (ringtone, send to voicemail)
250        // if we don't have a telephone
251        final MenuItem optionsSendToVoicemail = menu.findItem(R.id.menu_send_to_voicemail);
252        if (optionsSendToVoicemail != null) {
253            optionsSendToVoicemail.setChecked(mSendToVoicemailState);
254            optionsSendToVoicemail.setVisible(mOptionsMenuOptions);
255        }
256        final MenuItem optionsRingtone = menu.findItem(R.id.menu_set_ringtone);
257        if (optionsRingtone != null) {
258            optionsRingtone.setVisible(mOptionsMenuOptions);
259        }
260
261        final MenuItem editMenu = menu.findItem(R.id.menu_edit);
262        editMenu.setVisible(mOptionsMenuEditable);
263
264        final MenuItem deleteMenu = menu.findItem(R.id.menu_delete);
265        deleteMenu.setVisible(mOptionsMenuEditable);
266
267        final MenuItem shareMenu = menu.findItem(R.id.menu_share);
268        shareMenu.setVisible(mOptionsMenuShareable);
269
270        final MenuItem createContactShortcutMenu = menu.findItem(R.id.menu_create_contact_shortcut);
271        createContactShortcutMenu.setVisible(mOptionsMenuCanCreateShortcut);
272    }
273
274    public boolean isContactOptionsChangeEnabled() {
275        return mContactData != null && !mContactData.isDirectoryEntry()
276                && PhoneCapabilityTester.isPhone(mContext);
277    }
278
279    public boolean isContactEditable() {
280        return mContactData != null && !mContactData.isDirectoryEntry();
281    }
282
283    public boolean isContactShareable() {
284        return mContactData != null && !mContactData.isDirectoryEntry();
285    }
286
287    public boolean isContactCanCreateShortcut() {
288        return mContactData != null && !mContactData.isUserProfile()
289                && !mContactData.isDirectoryEntry();
290    }
291
292    @Override
293    public boolean onOptionsItemSelected(MenuItem item) {
294        switch (item.getItemId()) {
295            case R.id.menu_edit: {
296                if (mListener != null) mListener.onEditRequested(mLookupUri);
297                break;
298            }
299            case R.id.menu_delete: {
300                if (mListener != null) mListener.onDeleteRequested(mLookupUri);
301                return true;
302            }
303            case R.id.menu_set_ringtone: {
304                if (mContactData == null) return false;
305                doPickRingtone();
306                return true;
307            }
308            case R.id.menu_share: {
309                if (mContactData == null) return false;
310
311                final String lookupKey = mContactData.getLookupKey();
312                Uri shareUri = Uri.withAppendedPath(Contacts.CONTENT_VCARD_URI, lookupKey);
313                if (mContactData.isUserProfile()) {
314                    // User is sharing the profile.  We don't want to force the receiver to have
315                    // the highly-privileged READ_PROFILE permission, so we need to request a
316                    // pre-authorized URI from the provider.
317                    shareUri = getPreAuthorizedUri(shareUri);
318                }
319
320                final Intent intent = new Intent(Intent.ACTION_SEND);
321                intent.setType(Contacts.CONTENT_VCARD_TYPE);
322                intent.putExtra(Intent.EXTRA_STREAM, shareUri);
323
324                // Launch chooser to share contact via
325                final CharSequence chooseTitle = mContext.getText(R.string.share_via);
326                final Intent chooseIntent = Intent.createChooser(intent, chooseTitle);
327
328                try {
329                    mContext.startActivity(chooseIntent);
330                } catch (ActivityNotFoundException ex) {
331                    Toast.makeText(mContext, R.string.share_error, Toast.LENGTH_SHORT).show();
332                }
333                return true;
334            }
335            case R.id.menu_send_to_voicemail: {
336                // Update state and save
337                mSendToVoicemailState = !mSendToVoicemailState;
338                item.setChecked(mSendToVoicemailState);
339                Intent intent = ContactSaveService.createSetSendToVoicemail(
340                        mContext, mLookupUri, mSendToVoicemailState);
341                mContext.startService(intent);
342                return true;
343            }
344            case R.id.menu_create_contact_shortcut: {
345                // Create a launcher shortcut with this contact
346                createLauncherShortcutWithContact();
347                return true;
348            }
349        }
350        return false;
351    }
352
353    /**
354     * Creates a launcher shortcut with the current contact.
355     */
356    private void createLauncherShortcutWithContact() {
357        // Hold the parent activity of this fragment in case this fragment is destroyed
358        // before the callback to onShortcutIntentCreated(...)
359        final Activity parentActivity = getActivity();
360
361        ShortcutIntentBuilder builder = new ShortcutIntentBuilder(parentActivity,
362                new OnShortcutIntentCreatedListener() {
363
364            @Override
365            public void onShortcutIntentCreated(Uri uri, Intent shortcutIntent) {
366                // Broadcast the shortcutIntent to the launcher to create a
367                // shortcut to this contact
368                shortcutIntent.setAction(ACTION_INSTALL_SHORTCUT);
369                parentActivity.sendBroadcast(shortcutIntent);
370
371                // Send a toast to give feedback to the user that a shortcut to this
372                // contact was added to the launcher.
373                Toast.makeText(parentActivity,
374                        R.string.createContactShortcutSuccessful,
375                        Toast.LENGTH_SHORT).show();
376            }
377
378        });
379        builder.createContactShortcutIntent(mLookupUri);
380    }
381
382    /**
383     * Calls into the contacts provider to get a pre-authorized version of the given URI.
384     */
385    private Uri getPreAuthorizedUri(Uri uri) {
386        Bundle uriBundle = new Bundle();
387        uriBundle.putParcelable(ContactsContract.Authorization.KEY_URI_TO_AUTHORIZE, uri);
388        Bundle authResponse = mContext.getContentResolver().call(
389                ContactsContract.AUTHORITY_URI,
390                ContactsContract.Authorization.AUTHORIZATION_METHOD,
391                null,
392                uriBundle);
393        if (authResponse != null) {
394            return (Uri) authResponse.getParcelable(
395                    ContactsContract.Authorization.KEY_AUTHORIZED_URI);
396        } else {
397            return uri;
398        }
399    }
400
401    @Override
402    public boolean handleKeyDown(int keyCode) {
403        switch (keyCode) {
404            case KeyEvent.KEYCODE_DEL: {
405                if (mListener != null) mListener.onDeleteRequested(mLookupUri);
406                return true;
407            }
408        }
409        return false;
410    }
411
412    private void doPickRingtone() {
413
414        Intent intent = new Intent(RingtoneManager.ACTION_RINGTONE_PICKER);
415        // Allow user to pick 'Default'
416        intent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_DEFAULT, true);
417        // Show only ringtones
418        intent.putExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, RingtoneManager.TYPE_RINGTONE);
419        // Don't show 'Silent'
420        intent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_SILENT, false);
421
422        Uri ringtoneUri;
423        if (mCustomRingtone != null) {
424            ringtoneUri = Uri.parse(mCustomRingtone);
425        } else {
426            // Otherwise pick default ringtone Uri so that something is selected.
427            ringtoneUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE);
428        }
429
430        // Put checkmark next to the current ringtone for this contact
431        intent.putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, ringtoneUri);
432
433        // Launch!
434        startActivityForResult(intent, REQUEST_CODE_PICK_RINGTONE);
435    }
436
437    @Override
438    public void onActivityResult(int requestCode, int resultCode, Intent data) {
439        if (resultCode != Activity.RESULT_OK) {
440            return;
441        }
442
443        switch (requestCode) {
444            case REQUEST_CODE_PICK_RINGTONE: {
445                Uri pickedUri = data.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI);
446                handleRingtonePicked(pickedUri);
447                break;
448            }
449        }
450    }
451
452    private void handleRingtonePicked(Uri pickedUri) {
453        if (pickedUri == null || RingtoneManager.isDefault(pickedUri)) {
454            mCustomRingtone = null;
455        } else {
456            mCustomRingtone = pickedUri.toString();
457        }
458        Intent intent = ContactSaveService.createSetRingtone(
459                mContext, mLookupUri, mCustomRingtone);
460        mContext.startService(intent);
461    }
462
463    /** Toggles whether to load stream items. Just for debugging */
464    public void toggleLoadStreamItems() {
465        Loader<Contact> loaderObj = getLoaderManager().getLoader(LOADER_DETAILS);
466        ContactLoader loader = (ContactLoader) loaderObj;
467        loader.setLoadStreamItems(!loader.getLoadStreamItems());
468    }
469
470    /** Returns whether to load stream items. Just for debugging */
471    public boolean getLoadStreamItems() {
472        Loader<Contact> loaderObj = getLoaderManager().getLoader(LOADER_DETAILS);
473        ContactLoader loader = (ContactLoader) loaderObj;
474        return loader != null && loader.getLoadStreamItems();
475    }
476}
477