You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Android Studio中显示通话记录里的未知/未保存电话号码列表?

Got it, let's walk through how to build this feature step by step. I've tackled similar tasks before, so here's a practical, actionable breakdown:

实现展示未知/未保存号码通话记录的步骤

1. 申请必要权限

First, your app needs two critical permissions to access call logs and compare against saved contacts. Add these to your AndroidManifest.xml:

<uses-permission android:name="android.permission.READ_CALL_LOG" />
<uses-permission android:name="android.permission.READ_CONTACTS" />

Since Android 6.0 (API 23), you have to request these permissions dynamically at runtime. Here's a quick snippet to handle that in your Activity:

private static final int PERMISSION_REQUEST_CODE = 100;
private String[] REQUIRED_PERMISSIONS = {Manifest.permission.READ_CALL_LOG, Manifest.permission.READ_CONTACTS};

// Check and request permissions when the activity starts
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    
    if (!hasPermissions(this, REQUIRED_PERMISSIONS)) {
        ActivityCompat.requestPermissions(this, REQUIRED_PERMISSIONS, PERMISSION_REQUEST_CODE);
    } else {
        // Permissions granted, start processing call logs
        fetchAndFilterUnknownCalls();
    }
}

// Helper method to check permissions
private boolean hasPermissions(Context context, String... permissions) {
    for (String permission : permissions) {
        if (ContextCompat.checkSelfPermission(context, permission) != PackageManager.PERMISSION_GRANTED) {
            return false;
        }
    }
    return true;
}

// Handle permission request result
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
    super.onRequestPermissionsResult(requestCode, permissions, grantResults);
    if (requestCode == PERMISSION_REQUEST_CODE) {
        boolean allGranted = true;
        for (int result : grantResults) {
            if (result != PackageManager.PERMISSION_GRANTED) {
                allGranted = false;
                break;
            }
        }
        if (allGranted) {
            fetchAndFilterUnknownCalls();
        } else {
            // Show a message to user explaining why permissions are needed
            Toast.makeText(this, "Permissions are required to show unknown call records", Toast.LENGTH_LONG).show();
        }
    }
}

2. 查询通话记录

Next, use ContentResolver to fetch all call logs. We'll pull key details like the phone number, call type, and timestamp:

private List<CallRecord> fetchCallLogs() {
    List<CallRecord> callRecords = new ArrayList<>();
    ContentResolver contentResolver = getContentResolver();
    
    // Define the columns we need from call logs
    String[] projection = {
            CallLog.Calls.NUMBER,
            CallLog.Calls.TYPE,
            CallLog.Calls.DATE
    };
    
    Cursor cursor = contentResolver.query(
            CallLog.Calls.CONTENT_URI,
            projection,
            null,
            null,
            CallLog.Calls.DATE + " DESC" // Sort by most recent calls first
    );
    
    if (cursor != null) {
        while (cursor.moveToNext()) {
            String number = cursor.getString(cursor.getColumnIndexOrThrow(CallLog.Calls.NUMBER));
            int callType = cursor.getInt(cursor.getColumnIndexOrThrow(CallLog.Calls.TYPE));
            long date = cursor.getLong(cursor.getColumnIndexOrThrow(CallLog.Calls.DATE));
            
            // Normalize the number to avoid format mismatches (e.g., (123) 456-7890 vs 1234567890)
            String normalizedNumber = normalizePhoneNumber(number);
            
            callRecords.add(new CallRecord(normalizedNumber, callType, date));
        }
        cursor.close();
    }
    return callRecords;
}

// Helper to normalize phone numbers (remove non-digit characters except '+')
private String normalizePhoneNumber(String number) {
    return number.replaceAll("[^\\d+]", "");
}

// Simple data class to hold call record details
public static class CallRecord {
    public String number;
    public int callType;
    public long date;
    
    public CallRecord(String number, int callType, long date) {
        this.number = number;
        this.callType = callType;
        this.date = date;
    }
}

3. 筛选未知/未保存号码

Now we need to check which call numbers aren't in the user's saved contacts. To optimize performance, pre-fetch all contact numbers into a HashSet first (this avoids repeated database hits):

private HashSet<String> getAllContactNumbers() {
    HashSet<String> contactNumbers = new HashSet<>();
    ContentResolver contentResolver = getContentResolver();
    String[] projection = {ContactsContract.CommonDataKinds.Phone.NORMALIZED_NUMBER};
    
    Cursor cursor = contentResolver.query(
            ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
            projection,
            null,
            null,
            null
    );
    
    if (cursor != null) {
        while (cursor.moveToNext()) {
            String number = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.NORMALIZED_NUMBER));
            contactNumbers.add(number);
        }
        cursor.close();
    }
    return contactNumbers;
}

// Filter call logs to get only unknown numbers
private List<CallRecord> filterUnknownCalls(List<CallRecord> allCallRecords) {
    HashSet<String> contactNumbers = getAllContactNumbers();
    List<CallRecord> unknownCalls = new ArrayList<>();
    for (CallRecord record : allCallRecords) {
        if (!contactNumbers.contains(record.number)) {
            unknownCalls.add(record);
        }
    }
    return unknownCalls;
}

4. 展示筛选后的列表

Finally, display the filtered unknown calls using a RecyclerView (the modern choice for lists in Android):

Step 4.1: Add RecyclerView to your layout (activity_main.xml)

<androidx.recyclerview.widget.RecyclerView
    android:id="@+id/recycler_view"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:padding="8dp"/>

Step 4.2: Create a list item layout (item_call_record.xml)

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical"
    android:padding="16dp"
    android:background="?attr/selectableItemBackground">
    
    <TextView
        android:id="@+id/tv_number"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="18sp"
        android:textStyle="bold"/>
    
    <TextView
        android:id="@+id/tv_call_details"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="14sp"
        android:textColor="@android:color/darker_gray"
        android:layout_marginTop="4dp"/>
</LinearLayout>

Step 4.3: Implement the RecyclerView Adapter

public class CallRecordAdapter extends RecyclerView.Adapter<CallRecordAdapter.CallRecordViewHolder> {
    private List<CallRecord> callRecords;
    private Context context;
    
    public CallRecordAdapter(Context context, List<CallRecord> callRecords) {
        this.context = context;
        this.callRecords = callRecords;
    }
    
    @NonNull
    @Override
    public CallRecordViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(context).inflate(R.layout.item_call_record, parent, false);
        return new CallRecordViewHolder(view);
    }
    
    @Override
    public void onBindViewHolder(@NonNull CallRecordViewHolder holder, int position) {
        CallRecord record = callRecords.get(position);
        holder.tvNumber.setText(record.number);
        
        // Format call type and date
        String callType = getCallTypeString(record.callType);
        String dateString = new SimpleDateFormat("MM/dd/yyyy HH:mm", Locale.getDefault()).format(new Date(record.date));
        holder.tvCallDetails.setText(String.format("%s • %s", callType, dateString));
    }
    
    private String getCallTypeString(int callType) {
        switch (callType) {
            case CallLog.Calls.INCOMING_TYPE:
                return "Incoming";
            case CallLog.Calls.OUTGOING_TYPE:
                return "Outgoing";
            case CallLog.Calls.MISSED_TYPE:
                return "Missed";
            default:
                return "Unknown";
        }
    }
    
    @Override
    public int getItemCount() {
        return callRecords.size();
    }
    
    public static class CallRecordViewHolder extends RecyclerView.ViewHolder {
        TextView tvNumber;
        TextView tvCallDetails;
        
        public CallRecordViewHolder(@NonNull View itemView) {
            super(itemView);
            tvNumber = itemView.findViewById(R.id.tv_number);
            tvCallDetails = itemView.findViewById(R.id.tv_call_details);
        }
    }
}

Step 4.4: Set up RecyclerView in your Activity

Add this to your fetchAndFilterUnknownCalls() method (run on a background thread to avoid UI freezes):

private void fetchAndFilterUnknownCalls() {
    new AsyncTask<Void, Void, List<CallRecord>>() {
        @Override
        protected List<CallRecord> doInBackground(Void... voids) {
            List<CallRecord> allCalls = fetchCallLogs();
            return filterUnknownCalls(allCalls);
        }
        
        @Override
        protected void onPostExecute(List<CallRecord> unknownCalls) {
            RecyclerView recyclerView = findViewById(R.id.recycler_view);
            recyclerView.setLayoutManager(new LinearLayoutManager(MainActivity.this));
            recyclerView.setAdapter(new CallRecordAdapter(MainActivity.this, unknownCalls));
            
            if (unknownCalls.isEmpty()) {
                Toast.makeText(MainActivity.this, "No unknown call records found", Toast.LENGTH_SHORT).show();
            }
        }
    }.execute();
}

Important Notes

  • Android 10+ Permissions: For API level 29+, READ_CALL_LOG is a privileged permission on some devices. Handle cases where users deny it repeatedly with clear explanations.
  • Modern Alternatives: AsyncTask works for simple cases, but consider using Kotlin Coroutines or WorkManager for better lifecycle management in newer apps.
  • Number Normalization: Always normalize numbers before comparing—different regions use different formats, so stripping non-digit characters (except '+') ensures accurate matches.

内容的提问来源于stack exchange,提问作者Rick

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.14 08:08:20