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