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