• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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         displayIntent.setData(mContact.getContactLookupUri());
237         try {
238             getContext().startActivity(displayIntent);
239         } catch (ActivityNotFoundException e) {
240             Toast.makeText(getContext(),
241                            getContext().getString(R.string.fail_display_contact),
242                            Toast.LENGTH_LONG).show();
243             Log.w(TAG, "No contact app available to display the contact", e);
244             return;
245         }
246 
247     }
248 
249     /** Shows the dialog to remove the contact, restoring it from {@code state} if it's not null. */
showRemoveContactDialog(Bundle state)250     private void showRemoveContactDialog(Bundle state) {
251         if (mRemoveContactDialog == null) {
252             return;
253         }
254         if (state != null) {
255             mRemoveContactDialog.onRestoreInstanceState(state);
256         }
257         mRemoveContactDialog.show();
258     }
259 
260     @Override
onSaveInstanceState()261     protected Parcelable onSaveInstanceState() {
262         final Parcelable superState = super.onSaveInstanceState();
263         if (mRemoveContactDialog == null || !mRemoveContactDialog.isShowing()) {
264             return superState;
265         }
266         final SavedState myState = new SavedState(superState);
267         myState.isDialogShowing = true;
268         myState.dialogBundle = mRemoveContactDialog.onSaveInstanceState();
269         return myState;
270     }
271 
272     @Override
onRestoreInstanceState(Parcelable state)273     protected void onRestoreInstanceState(Parcelable state) {
274         if (state == null || !state.getClass().equals(SavedState.class)) {
275             // Didn't save state for us in onSaveInstanceState
276             super.onRestoreInstanceState(state);
277             return;
278         }
279         SavedState myState = (SavedState) state;
280         super.onRestoreInstanceState(myState.getSuperState());
281         if (myState.isDialogShowing) {
282             showRemoveContactDialog(myState.dialogBundle);
283         }
284     }
285 
286     private static class SavedState extends BaseSavedState {
287         boolean isDialogShowing;
288         Bundle dialogBundle;
289 
SavedState(Parcel source)290         public SavedState(Parcel source) {
291             super(source);
292             isDialogShowing = source.readInt() == 1;
293             dialogBundle = source.readBundle();
294         }
295 
296         @Override
writeToParcel(Parcel dest, int flags)297         public void writeToParcel(Parcel dest, int flags) {
298             super.writeToParcel(dest, flags);
299             dest.writeInt(isDialogShowing ? 1 : 0);
300             dest.writeBundle(dialogBundle);
301         }
302 
SavedState(Parcelable superState)303         public SavedState(Parcelable superState) {
304             super(superState);
305         }
306 
307         public static final Parcelable.Creator<SavedState> CREATOR =
308                 new Parcelable.Creator<SavedState>() {
309                     public SavedState createFromParcel(Parcel in) {
310                         return new SavedState(in);
311                     }
312 
313                     public SavedState[] newArray(int size) {
314                         return new SavedState[size];
315                     }
316                 };
317     }
318 }
319