1 /*
2  * Copyright (C) 2016 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 package com.android.emergency.preferences;
17 
18 import android.app.AlertDialog;
19 import android.content.ActivityNotFoundException;
20 import android.content.ComponentName;
21 import android.content.Context;
22 import android.content.DialogInterface;
23 import android.content.Intent;
24 import android.content.pm.PackageManager;
25 import android.content.pm.ResolveInfo;
26 import android.graphics.drawable.Drawable;
27 import android.net.Uri;
28 import android.os.Bundle;
29 import android.os.Parcel;
30 import android.os.Parcelable;
31 import androidx.annotation.NonNull;
32 import androidx.annotation.Nullable;
33 import androidx.preference.Preference;
34 import androidx.preference.PreferenceViewHolder;
35 import android.telecom.TelecomManager;
36 import android.text.BidiFormatter;
37 import android.text.TextDirectionHeuristics;
38 import android.util.AttributeSet;
39 import android.util.Log;
40 import android.view.View;
41 import android.widget.ImageView;
42 import android.widget.Toast;
43 
44 import com.android.emergency.CircleFramedDrawable;
45 import com.android.emergency.EmergencyContactManager;
46 import com.android.emergency.R;
47 import com.android.internal.annotations.VisibleForTesting;
48 import com.android.internal.logging.MetricsLogger;
49 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
50 
51 import java.util.List;
52 
53 
54 /**
55  * A {@link Preference} to display or call a contact using the specified URI string.
56  */
57 public class ContactPreference extends Preference {
58 
59     private static final String TAG = "ContactPreference";
60 
61     static final ContactFactory DEFAULT_CONTACT_FACTORY = new ContactFactory() {
62         @Override
63         public EmergencyContactManager.Contact getContact(Context context, Uri phoneUri) {
64             return EmergencyContactManager.getContact(context, phoneUri);
65         }
66     };
67 
68     private final ContactFactory mContactFactory;
69     private EmergencyContactManager.Contact mContact;
70     @Nullable private RemoveContactPreferenceListener mRemoveContactPreferenceListener;
71     @Nullable private AlertDialog mRemoveContactDialog;
72 
73     /**
74      * Listener for removing a contact.
75      */
76     public interface RemoveContactPreferenceListener {
77         /**
78          * Callback to remove a contact preference.
79          */
onRemoveContactPreference(ContactPreference preference)80         void onRemoveContactPreference(ContactPreference preference);
81     }
82 
83     /**
84      * Interface for getting a contact for a phone number Uri.
85      */
86     public interface ContactFactory {
87         /**
88          * Gets a {@link EmergencyContactManager.Contact} for a phone {@link Uri}.
89          *
90          * @param context The context to use.
91          * @param phoneUri The phone uri.
92          * @return a contact for the given phone uri.
93          */
getContact(Context context, Uri phoneUri)94         EmergencyContactManager.Contact getContact(Context context, Uri phoneUri);
95     }
96 
ContactPreference(Context context, AttributeSet attributes)97     public ContactPreference(Context context, AttributeSet attributes) {
98         super(context, attributes);
99         mContactFactory = DEFAULT_CONTACT_FACTORY;
100     }
101 
102     /**
103      * Instantiates a ContactPreference that displays an emergency contact, taking in a Context and
104      * the Uri.
105      */
ContactPreference(Context context, @NonNull Uri phoneUri)106     public ContactPreference(Context context, @NonNull Uri phoneUri) {
107         this(context, phoneUri, DEFAULT_CONTACT_FACTORY);
108     }
109 
110     @VisibleForTesting
ContactPreference(Context context, @NonNull Uri phoneUri, @NonNull ContactFactory contactFactory)111     ContactPreference(Context context, @NonNull Uri phoneUri,
112             @NonNull ContactFactory contactFactory) {
113         super(context);
114         mContactFactory = contactFactory;
115         setOrder(DEFAULT_ORDER);
116 
117         setPhoneUri(phoneUri);
118 
119         setWidgetLayoutResource(R.layout.preference_user_action_widget);
120         setPersistent(false);
121     }
122 
setPhoneUri(@onNull Uri phoneUri)123     public void setPhoneUri(@NonNull Uri phoneUri) {
124         if (mContact != null && !phoneUri.equals(mContact.getPhoneUri()) &&
125                 mRemoveContactDialog != null) {
126             mRemoveContactDialog.dismiss();
127         }
128         mContact = mContactFactory.getContact(getContext(), phoneUri);
129 
130         setTitle(mContact.getName());
131         setKey(mContact.getPhoneUri().toString());
132         String summary = mContact.getPhoneType() == null ?
133                 mContact.getPhoneNumber() :
134                 String.format(
135                         getContext().getResources().getString(R.string.phone_type_and_phone_number),
136                         mContact.getPhoneType(),
137                         BidiFormatter.getInstance().unicodeWrap(mContact.getPhoneNumber(),
138                                 TextDirectionHeuristics.LTR));
139         setSummary(summary);
140 
141         // Update the message to show the correct name.
142         if (mRemoveContactDialog != null) {
143             mRemoveContactDialog.setMessage(
144                     String.format(getContext().getString(R.string.remove_contact),
145                             mContact.getName()));
146         }
147 
148         //TODO: Consider doing the following in a non-UI thread.
149         Drawable icon;
150         if (mContact.getPhoto() != null) {
151             icon = new CircleFramedDrawable(mContact.getPhoto(),
152                     (int) getContext().getResources().getDimension(R.dimen.circle_avatar_size));
153         } else {
154             icon = getContext().getDrawable(R.drawable.ic_account_circle_filled_24dp);
155         }
156         setIcon(icon);
157     }
158 
159     /** Listener to be informed when a contact preference should be deleted. */
setRemoveContactPreferenceListener( RemoveContactPreferenceListener removeContactListener)160     public void setRemoveContactPreferenceListener(
161             RemoveContactPreferenceListener removeContactListener) {
162         mRemoveContactPreferenceListener = removeContactListener;
163         if (mRemoveContactPreferenceListener == null) {
164             mRemoveContactDialog = null;
165             return;
166         }
167         if (mRemoveContactDialog != null) {
168             return;
169         }
170         // Create the remove contact dialog
171         AlertDialog.Builder builder = new AlertDialog.Builder(getContext());
172         builder.setNegativeButton(getContext().getString(R.string.cancel), null);
173         builder.setPositiveButton(getContext().getString(R.string.remove),
174                 new DialogInterface.OnClickListener() {
175                     @Override
176                     public void onClick(DialogInterface dialogInterface,
177                                         int which) {
178                         if (mRemoveContactPreferenceListener != null) {
179                             mRemoveContactPreferenceListener
180                                     .onRemoveContactPreference(ContactPreference.this);
181                         }
182                     }
183                 });
184         builder.setMessage(String.format(getContext().getString(R.string.remove_contact),
185                 mContact.getName()));
186         mRemoveContactDialog = builder.create();
187     }
188 
189     @Override
onBindViewHolder(PreferenceViewHolder holder)190     public void onBindViewHolder(PreferenceViewHolder holder) {
191         super.onBindViewHolder(holder);
192         View deleteContactIcon = holder.findViewById(R.id.delete_contact);
193         View callContactIcon = holder.findViewById(R.id.call_contact);
194         if (mRemoveContactPreferenceListener == null) {
195             // Default icon is delete, change icon to phone when ContactPreference binding
196             // ViewEmergencyContactsFragment.
197             deleteContactIcon.setVisibility(View.GONE);
198             callContactIcon.setVisibility(View.VISIBLE);
199         } else {
200             deleteContactIcon.setOnClickListener((View view) -> {
201                 showRemoveContactDialog(null);
202             });
203         }
204     }
205 
getPhoneUri()206     public Uri getPhoneUri() {
207         return mContact.getPhoneUri();
208     }
209 
210     @VisibleForTesting
getContact()211     EmergencyContactManager.Contact getContact() {
212         return mContact;
213     }
214 
215     @VisibleForTesting
getRemoveContactDialog()216     AlertDialog getRemoveContactDialog() {
217         return mRemoveContactDialog;
218     }
219 
220     /**
221      * Calls the contact.
222      */
callContact()223     public void callContact() {
224         // Use TelecomManager to place the call; this APK has CALL_PRIVILEGED permission so it will
225         // be able to call emergency numbers.
226         TelecomManager tm = (TelecomManager) getContext().getSystemService(Context.TELECOM_SERVICE);
227         tm.placeCall(Uri.parse("tel:" + mContact.getPhoneNumber()), null);
228         MetricsLogger.action(getContext(), MetricsEvent.ACTION_CALL_EMERGENCY_CONTACT);
229     }
230 
231     /**
232      * Displays a contact card for the contact.
233      */
displayContact()234     public void displayContact() {
235         Intent displayIntent = new Intent(Intent.ACTION_VIEW)
236                 .setData(mContact.getContactLookupUri())
237                 .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
238         try {
239             getContext().startActivity(displayIntent);
240         } catch (ActivityNotFoundException e) {
241             Toast.makeText(getContext(),
242                            getContext().getString(R.string.fail_display_contact),
243                            Toast.LENGTH_LONG).show();
244             Log.w(TAG, "No contact app available to display the contact", e);
245             return;
246         }
247 
248     }
249 
250     /** Shows the dialog to remove the contact, restoring it from {@code state} if it's not null. */
showRemoveContactDialog(Bundle state)251     private void showRemoveContactDialog(Bundle state) {
252         if (mRemoveContactDialog == null) {
253             return;
254         }
255         if (state != null) {
256             mRemoveContactDialog.onRestoreInstanceState(state);
257         }
258         mRemoveContactDialog.show();
259     }
260 
261     @Override
onSaveInstanceState()262     protected Parcelable onSaveInstanceState() {
263         final Parcelable superState = super.onSaveInstanceState();
264         if (mRemoveContactDialog == null || !mRemoveContactDialog.isShowing()) {
265             return superState;
266         }
267         final SavedState myState = new SavedState(superState);
268         myState.isDialogShowing = true;
269         myState.dialogBundle = mRemoveContactDialog.onSaveInstanceState();
270         return myState;
271     }
272 
273     @Override
onRestoreInstanceState(Parcelable state)274     protected void onRestoreInstanceState(Parcelable state) {
275         if (state == null || !state.getClass().equals(SavedState.class)) {
276             // Didn't save state for us in onSaveInstanceState
277             super.onRestoreInstanceState(state);
278             return;
279         }
280         SavedState myState = (SavedState) state;
281         super.onRestoreInstanceState(myState.getSuperState());
282         if (myState.isDialogShowing) {
283             showRemoveContactDialog(myState.dialogBundle);
284         }
285     }
286 
287     private static class SavedState extends BaseSavedState {
288         boolean isDialogShowing;
289         Bundle dialogBundle;
290 
SavedState(Parcel source)291         public SavedState(Parcel source) {
292             super(source);
293             isDialogShowing = source.readInt() == 1;
294             dialogBundle = source.readBundle();
295         }
296 
297         @Override
writeToParcel(Parcel dest, int flags)298         public void writeToParcel(Parcel dest, int flags) {
299             super.writeToParcel(dest, flags);
300             dest.writeInt(isDialogShowing ? 1 : 0);
301             dest.writeBundle(dialogBundle);
302         }
303 
SavedState(Parcelable superState)304         public SavedState(Parcelable superState) {
305             super(superState);
306         }
307 
308         public static final Parcelable.Creator<SavedState> CREATOR =
309                 new Parcelable.Creator<SavedState>() {
310                     public SavedState createFromParcel(Parcel in) {
311                         return new SavedState(in);
312                     }
313 
314                     public SavedState[] newArray(int size) {
315                         return new SavedState[size];
316                     }
317                 };
318     }
319 }
320