WelcomeFragment
服务条款页面是WelcomeFragment
EnterPhoneNumberFragment
电话号码输入页面是EnterPhoneNumberFragment:
开屏页,手机号,验证码,PIN设置,注册,登录
安装完成后第一次进入app后显示的闪屏页面是PassphraseCreateActivity,布局是R.layout.fragment_registration_blank
注册完成页面RegistrationCompleteFragment和WelcomeFragment欢迎页面都会显示闪屏页面:R.layout.fragment_registration_blank
WelcomeFragment:这个页面有两个功能,闪屏页面以及用户协议确认页面
RegistrationNavigationActivity管理着注册登录流程相关的多个fragment:
- WelcomeFragment
- EnterPhoneNumberFragment
- CountryPickerFragment
- EnterCodeFragment
- RegistrationCompleteFragment
- 等等
具体见:registration.xml
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/signup"
app:startDestination="@id/welcomeFragment">
<fragment
android:id="@+id/welcomeFragment"
android:name="org.thoughtcrime.securesms.registration.fragments.WelcomeFragment"
android:label="fragment_welcome"
tools:layout="@layout/fragment_registration_welcome">
<action
android:id="@+id/action_restore"
app:destination="@id/restoreBackupFragment"
app:enterAnim="@anim/nav_default_enter_anim"
app:exitAnim="@anim/nav_default_exit_anim"
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
app:popExitAnim="@anim/nav_default_pop_exit_anim" />
<action
android:id="@+id/action_skip_restore"
app:destination="@id/enterPhoneNumberFragment"
app:enterAnim="@anim/nav_default_enter_anim"
app:exitAnim="@anim/nav_default_exit_anim"
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
app:popExitAnim="@anim/nav_default_pop_exit_anim" />
</fragment>
<fragment
android:id="@+id/enterPhoneNumberFragment"
android:name="org.thoughtcrime.securesms.registration.fragments.EnterPhoneNumberFragment"
android:label="fragment_enter_phone_number"
tools:layout="@layout/fragment_registration_enter_phone_number">
<action
android:id="@+id/action_pickCountry"
app:destination="@id/countryPickerFragment"
app:enterAnim="@anim/nav_default_enter_anim"
app:exitAnim="@anim/nav_default_exit_anim"
app:launchSingleTop="true"
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
app:popExitAnim="@anim/nav_default_pop_exit_anim" />
<action
android:id="@+id/action_enterVerificationCode"
app:destination="@id/enterCodeFragment"
app:enterAnim="@anim/nav_default_enter_anim"
app:exitAnim="@anim/nav_default_exit_anim"
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
app:popExitAnim="@anim/nav_default_pop_exit_anim" />
<action
android:id="@+id/action_requestCaptcha"
app:destination="@id/captchaFragment"
app:enterAnim="@anim/nav_default_enter_anim"
app:exitAnim="@anim/nav_default_exit_anim"
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
app:popExitAnim="@anim/nav_default_pop_exit_anim" />
</fragment>
<fragment
android:id="@+id/countryPickerFragment"
android:name="org.thoughtcrime.securesms.registration.fragments.CountryPickerFragment"
android:label="fragment_country_picker"
tools:layout="@layout/fragment_registration_country_picker">
<action
android:id="@+id/action_countrySelected"
app:popUpTo="@id/countryPickerFragment"
app:popUpToInclusive="true" />
</fragment>
<fragment
android:id="@+id/enterCodeFragment"
android:name="org.thoughtcrime.securesms.registration.fragments.EnterCodeFragment"
android:label="fragment_enter_code"
tools:layout="@layout/fragment_registration_enter_code">
<action
android:id="@+id/action_requireKbsLockPin"
app:destination="@id/registrationLockFragment"
app:enterAnim="@anim/nav_default_enter_anim"
app:exitAnim="@anim/nav_default_exit_anim"
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
app:popExitAnim="@anim/nav_default_pop_exit_anim"
app:popUpTo="@+id/welcomeFragment"
app:popUpToInclusive="true" />
<action
android:id="@+id/action_wrongNumber"
app:popUpTo="@id/enterCodeFragment"
app:popUpToInclusive="true" />
<action
android:id="@+id/action_requestCaptcha"
app:destination="@id/captchaFragment"
app:enterAnim="@anim/nav_default_enter_anim"
app:exitAnim="@anim/nav_default_exit_anim"
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
app:popExitAnim="@anim/nav_default_pop_exit_anim" />
<action
android:id="@+id/action_successfulRegistration"
app:destination="@id/registrationCompletePlaceHolderFragment"
app:popUpTo="@+id/welcomeFragment"
app:popUpToInclusive="true" />
<action
android:id="@+id/action_accountLocked"
app:destination="@id/accountLockedFragment"
app:enterAnim="@anim/nav_default_enter_anim"
app:exitAnim="@anim/nav_default_exit_anim"
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
app:popExitAnim="@anim/nav_default_pop_exit_anim"
app:popUpTo="@+id/welcomeFragment"
app:popUpToInclusive="true" />
</fragment>
<fragment
android:id="@+id/registrationLockFragment"
android:name="org.thoughtcrime.securesms.registration.fragments.RegistrationLockFragment"
android:label="fragment_kbs_lock"
tools:layout="@layout/fragment_registration_lock">
<action
android:id="@+id/action_successfulRegistration"
app:destination="@id/registrationCompletePlaceHolderFragment"
app:enterAnim="@anim/nav_default_enter_anim"
app:exitAnim="@anim/nav_default_exit_anim"
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
app:popExitAnim="@anim/nav_default_pop_exit_anim"
app:popUpTo="@+id/welcomeFragment"
app:popUpToInclusive="true" />
<action
android:id="@+id/action_accountLocked"
app:destination="@id/accountLockedFragment"
app:enterAnim="@anim/nav_default_enter_anim"
app:exitAnim="@anim/nav_default_exit_anim"
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
app:popExitAnim="@anim/nav_default_pop_exit_anim"
app:popUpTo="@+id/welcomeFragment"
app:popUpToInclusive="true" />
<argument
android:name="timeRemaining"
app:argType="long" />
<argument
android:name="isV1RegistrationLock"
app:argType="boolean" />
</fragment>
<fragment
android:id="@+id/accountLockedFragment"
android:name="org.thoughtcrime.securesms.registration.fragments.AccountLockedFragment"
android:label="fragment_account_locked"
tools:layout="@layout/account_locked_fragment"/>
<fragment
android:id="@+id/captchaFragment"
android:name="org.thoughtcrime.securesms.registration.fragments.CaptchaFragment"
android:label="fragment_captcha"
tools:layout="@layout/fragment_registration_captcha">
<action
android:id="@+id/action_captchaComplete"
app:popUpTo="@id/captchaFragment"
app:popUpToInclusive="true" />
</fragment>
<fragment
android:id="@+id/restoreBackupFragment"
android:name="org.thoughtcrime.securesms.registration.fragments.RestoreBackupFragment"
android:label="fragment_restore_backup"
tools:layout="@layout/fragment_registration_restore_backup">
<action
android:id="@+id/action_backupRestored"
app:destination="@id/enterPhoneNumberFragment"
app:enterAnim="@anim/nav_default_enter_anim"
app:exitAnim="@anim/nav_default_exit_anim"
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
app:popExitAnim="@anim/nav_default_pop_exit_anim"
app:popUpTo="@id/restoreBackupFragment"
app:popUpToInclusive="true" />
<action
android:id="@+id/action_skip"
app:destination="@id/enterPhoneNumberFragment"
app:enterAnim="@anim/slide_from_end"
app:exitAnim="@anim/slide_to_start"
app:popEnterAnim="@anim/slide_from_start"
app:popExitAnim="@anim/slide_to_end" />
<action
android:id="@+id/action_noBackupFound"
app:destination="@id/enterPhoneNumberFragment"
app:enterAnim="@anim/nav_default_enter_anim"
app:exitAnim="@anim/nav_default_exit_anim"
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
app:popExitAnim="@anim/nav_default_pop_exit_anim"
app:popUpTo="@id/restoreBackupFragment"
app:popUpToInclusive="true" />
<action
android:id="@+id/action_skip_no_return"
app:destination="@id/enterPhoneNumberFragment"
app:enterAnim="@anim/nav_default_enter_anim"
app:exitAnim="@anim/nav_default_exit_anim"
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
app:popExitAnim="@anim/nav_default_pop_exit_anim"
app:popUpTo="@+id/restoreBackupFragment"
app:popUpToInclusive="true" />
</fragment>
<fragment
android:id="@+id/registrationCompletePlaceHolderFragment"
android:name="org.thoughtcrime.securesms.registration.fragments.RegistrationCompleteFragment"
android:label="fragment_registration_complete_place_holder"
tools:layout="@layout/fragment_registration_blank" />
</navigation>
EnterPhoneNumberFragment 输入手机号,点击NEXT按钮-> handleRegister() -> handleRequestVerification()验证手机号 -> 根据需要判断是否要进行图片验证CaptchaFragment -> EnterPhoneNumberFragment#requestVerificationCode()请求验证码 -> RegistrationCodeRequest#requestSmsVerificationCode
EnterCodeFragment -> RegistrationCompleteFragment# onViewCreated()
//RegistrationCompleteFragment.java
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
FragmentActivity activity = requireActivity();
if (SignalStore.storageServiceValues().needsAccountRestore()) {
activity.startActivity(new Intent(activity, PinRestoreActivity.class));
} else if (!isReregister()) {
final Intent main = new Intent(activity, MainActivity.class);
final Intent profile = EditProfileActivity.getIntent(activity, false);
Intent kbs = CreateKbsPinActivity.getIntentForPinCreate(requireContext());
activity.startActivity(chainIntents(chainIntents(profile, kbs), main));
}
activity.finish();
ActivityNavigator.applyPopAnimationsToPendingTransition(activity);
}
会进行判断,如果还没有设置Pin,则跳到Pin设置页面PinRestoreActivity,PinRestoreEntryFragment;pinButton就是CONTINUE按钮,onPinSubmitted()
否则如果设置了Pin,且不是注册的话则启动MainActivity
点击CONTINUE按钮执行onPinSubmitted()时会根据FCM token来执行不同的逻辑
//PinRestoreViewModel.java
void onPinSubmitted(@NonNull String pin, @NonNull PinKeyboardType pinKeyboardType) {
int trimmedLength = pin.replace(" ", "").length();
if (trimmedLength == 0) {
event.postValue(Event.EMPTY_PIN);
return;
}
if (trimmedLength < KbsConstants.MINIMUM_PIN_LENGTH) {
event.postValue(Event.PIN_TOO_SHORT);
return;
}
if (tokenData != null) {
repo.submitPin(pin, tokenData, result -> {
switch (result.getResult()) {
case SUCCESS:
SignalStore.pinValues().setKeyboardType(pinKeyboardType);
SignalStore.storageServiceValues().setNeedsAccountRestore(false);
event.postValue(Event.SUCCESS);
break;
case LOCKED:
event.postValue(Event.PIN_LOCKED);
break;
case INCORRECT:
event.postValue(Event.PIN_INCORRECT);
updateTokenData(result.getTokenData(), true);
break;
case NETWORK_ERROR:
event.postValue(Event.NETWORK_ERROR);
break;
}
});
} else {
repo.getToken(token -> {
if (token.isPresent()) {
updateTokenData(token.get(), false);
onPinSubmitted(pin, pinKeyboardType);
} else {
event.postValue(Event.NETWORK_ERROR);
}
});
}
}
进入这个页面的时候就会获取token
public class PinRestoreViewModel extends ViewModel {
private final PinRestoreRepository repo;
private final DefaultValueLiveData<TriesRemaining> triesRemaining;
private final SingleLiveEvent<Event> event;
private volatile PinRestoreRepository.TokenData tokenData;
public PinRestoreViewModel() {
this.repo = new PinRestoreRepository();
this.triesRemaining = new DefaultValueLiveData<>(new TriesRemaining(10, false));
this.event = new SingleLiveEvent<>();
repo.getToken(token -> {
if (token.isPresent()) {
updateTokenData(token.get(), false);
} else {
event.postValue(Event.NETWORK_ERROR);
}
});
}
Megaphone
首页的Megaphone提示框:
相关类是Megaphone
RegistrationCompleteFragment
注册完成之后进入:RegistrationCompleteFragment 页面
注册时的输入手机号后请求验证码和验证码校验接口都需要带上Credentials:
//RegistrationService.java
public final class RegistrationService {
private final Credentials credentials;
private RegistrationService(@NonNull Credentials credentials) {
this.credentials = credentials;
}
public static RegistrationService getInstance(@NonNull String e164number, @NonNull String password) {
return new RegistrationService(new Credentials(e164number, password));
}
/**
* See {@link RegistrationCodeRequest}.
*/
public void requestVerificationCode(@NonNull Activity activity,
@NonNull RegistrationCodeRequest.Mode mode,
@Nullable String captchaToken,
@NonNull RegistrationCodeRequest.SmsVerificationCodeCallback callback)
{
RegistrationCodeRequest.requestSmsVerificationCode(activity, credentials, captchaToken, mode, callback);
}
/**
* See {@link CodeVerificationRequest}.
*/
public void verifyAccount(@NonNull Activity activity,
@Nullable String fcmToken,
@NonNull String code,
@Nullable String pin,
@Nullable PinRestoreRepository.TokenData tokenData,
@NonNull CodeVerificationRequest.VerifyCallback callback)
{
CodeVerificationRequest.verifyAccount(activity, credentials, fcmToken, credentials.getE164number(),code, pin, tokenData, callback);
}
...
Credentials是根据手机号e164number和password创建的:
//Credentials.java
public final class Credentials {
private final String e164number;
private final String password;
public Credentials(@NonNull String e164number, @NonNull String password) {
this.e164number = e164number;
this.password = password;
}
public @NonNull String getE164number() {
return e164number;
}
public @NonNull String getPassword() {
return password;
}
}
password来自RegistrationViewModel的secret:
//EnterCodeFragment.java
...
RegistrationService registrationService = RegistrationService.getInstance(model.getNumber().getE164Number(), model.getRegistrationSecret());
...
//RegistrationViewModel.java
private final String secret;
...
public RegistrationViewModel(@NonNull SavedStateHandle savedStateHandle) {
secret = loadValue(savedStateHandle, "REGISTRATION_SECRET", Util.getSecret(18));
number = savedStateHandle.getLiveData("NUMBER", NumberViewState.INITIAL);
textCodeEntered = savedStateHandle.getLiveData("TEXT_CODE_ENTERED", "");
captchaToken = savedStateHandle.getLiveData("CAPTCHA");
fcmToken = savedStateHandle.getLiveData("FCM_TOKEN");
restoreFlowShown = savedStateHandle.getLiveData("RESTORE_FLOW_SHOWN", false);
successfulCodeRequestAttempts = savedStateHandle.getLiveData("SUCCESSFUL_CODE_REQUEST_ATTEMPTS", 0);
requestLimiter = savedStateHandle.getLiveData("REQUEST_RATE_LIMITER", new LocalCodeRequestRateLimiter(60_000));
kbsTokenData = savedStateHandle.getLiveData("KBS_TOKEN");
lockedTimeRemaining = savedStateHandle.getLiveData("TIME_REMAINING", 0L);
canCallAtTime = savedStateHandle.getLiveData("CAN_CALL_AT_TIME", 0L);
}
secret 是根据Util.getSecret(18)
生成的:
//Util.java
...
public static String getSecret(int size) {
byte[] secret = getSecretBytes(size);
return Base64.encodeBytes(secret);
}
public static byte[] getSecretBytes(int size) {
return getSecretBytes(new SecureRandom(), size);
}
...
即根据随机数生成的一个secret。
即客户端注册时带上的Credentials中的password是客户端根据随机数生成的。
手机号输入页面
手机号注册页面 EnterPhoneNumberFragment
点击注册按钮:
//EnterPhoneNumberFragment.java
private void handleRegister(@NonNull Context context) {
if (TextUtils.isEmpty(countryCode.getText())) {
Toast.makeText(context, getString(R.string.RegistrationActivity_you_must_specify_your_country_code), Toast.LENGTH_LONG).show();
return;
}
if (TextUtils.isEmpty(this.number.getText())) {
Toast.makeText(context, getString(R.string.RegistrationActivity_you_must_specify_your_phone_number), Toast.LENGTH_LONG).show();
return;
}
final NumberViewState number = getModel().getNumber();
final String e164number = number.getE164Number();
if (!number.isValid()) {
Dialogs.showAlertDialog(context,
getString(R.string.RegistrationActivity_invalid_number),
String.format(getString(R.string.RegistrationActivity_the_number_you_specified_s_is_invalid), e164number));
return;
}
PlayServicesUtil.PlayServicesStatus fcmStatus = PlayServicesUtil.getPlayServicesStatus(context);
if (fcmStatus == PlayServicesUtil.PlayServicesStatus.SUCCESS) {
handleRequestVerification(context, e164number, true);
} else if (fcmStatus == PlayServicesUtil.PlayServicesStatus.MISSING) {
handlePromptForNoPlayServices(context, e164number);
} else if (fcmStatus == PlayServicesUtil.PlayServicesStatus.NEEDS_UPDATE) {
GoogleApiAvailability.getInstance().getErrorDialog(requireActivity(), ConnectionResult.SERVICE_VERSION_UPDATE_REQUIRED, 0).show();
} else {
Dialogs.showAlertDialog(context, getString(R.string.RegistrationActivity_play_services_error),
getString(R.string.RegistrationActivity_google_play_services_is_updating_or_unavailable));
}
}
EnterPhoneNumberFragment#requestVerificationCode:
private void requestVerificationCode(String e164number, @NonNull RegistrationCodeRequest.Mode mode) {
RegistrationViewModel model = getModel();
String captcha = model.getCaptchaToken();
model.clearCaptchaResponse();
NavController navController = Navigation.findNavController(register);
if (!model.getRequestLimiter().canRequest(mode, e164number, System.currentTimeMillis())) {
Log.i(TAG, "Local rate limited");
navController.navigate(EnterPhoneNumberFragmentDirections.actionEnterVerificationCode());
cancelSpinning(register);
enableAllEntries();
return;
}
RegistrationService registrationService = RegistrationService.getInstance(e164number, model.getRegistrationSecret());
registrationService.requestVerificationCode(requireActivity(), mode, captcha,
new RegistrationCodeRequest.SmsVerificationCodeCallback() {
@Override
public void onNeedCaptcha() {
if (getContext() == null) {
Log.i(TAG, "Got onNeedCaptcha response, but fragment is no longer attached.");
return;
}
navController.navigate(EnterPhoneNumberFragmentDirections.actionRequestCaptcha());
cancelSpinning(register);
enableAllEntries();
model.getRequestLimiter().onUnsuccessfulRequest();
model.updateLimiter();
}
...
RegistrationService#requestVerificationCode
RegistrationCodeRequest.requestSmsVerificationCode
点击NEXT按钮,调用的是PushServiceSocket#requestSmsVerificationCode
请求的接口是 /v1/accounts/sms/code/%s?client=%s
验证码输入页面
输入验证码页面 EnterCodeFragment
验证码输入未完成,调用的是PushServiceSocket#verifyAccountCode,请求的接口是:/v1/accounts/code/%s
验证码输入完成:
private void setOnCodeFullyEnteredListener(VerificationCodeView verificationCodeView) {
verificationCodeView.setOnCompleteListener(code -> {
RegistrationViewModel model = getModel();
model.onVerificationCodeEntered(code);
callMeCountDown.setVisibility(View.INVISIBLE);
wrongNumber.setVisibility(View.INVISIBLE);
keyboard.displayProgress();
RegistrationService registrationService = RegistrationService.getInstance(model.getNumber().getE164Number(), model.getRegistrationSecret());
registrationService.verifyAccount(requireActivity(), model.getFcmToken(), code, null, null, null,
new CodeVerificationRequest.VerifyCallback() {
@Override
public void onSuccessfulRegistration() {
keyboard.displaySuccess().addListener(new AssertedSuccessListener<Boolean>() {
@Override
public void onSuccess(Boolean result) {
handleSuccessfulRegistration();
}
});
}
...
执行registrationService.verifyAccount,验证成功后执行handleSuccessfulRegistration():
private void handleSuccessfulRegistration() {
Navigation.findNavController(requireView()).navigate(EnterCodeFragmentDirections.actionSuccessfulRegistration());
}
即注册成功后进入会一个闪屏页面:RegistrationCompleteFragment,然后会处理一些逻辑,并进入MainActivity:
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
FragmentActivity activity = requireActivity();
/*
//设置pin码的功能先不要
if (SignalStore.storageServiceValues().needsAccountRestore()) {
activity.startActivity(new Intent(activity, PinRestoreActivity.class));
} else if (!isReregister()) {
final Intent main = new Intent(activity, MainActivity.class);
final Intent profile = EditProfileActivity.getIntentForUserProfile(activity);
Intent kbs = CreateKbsPinActivity.getIntentForPinCreate(requireContext());
activity.startActivity(chainIntents(chainIntents(profile, kbs), main));
}
*/
/* final Intent main = new Intent(activity, MainActivity.class);
activity.startActivity(main);*/
final Intent main = new Intent(activity, WalletRegistreAndImportActivity.class);
activity.startActivity(main);
activity.finish();
ActivityNavigator.applyPopAnimationsToPendingTransition(activity);
}
登录
项目中建立websocket连接前,必须先发送https请求登录成功,登录成功后会发送https请求获取证书,然后携带着证书去向服务器建立websocket连接,如果证书无效或者已经过期,则建立websocket连接时的握手过程会失败,服务器会返回403状态码的消息,正常应该会返回101的状态码。
会话列表页面
ConversationListFragment ,布局文件是:conversation_list_fragment.xml
顶部的搜索栏是DarkOverflowToolbar
ConversationListFragment 属于 MainActivity,点击会话列表的item,执行的是onConversationClick(注意不是onConversationClicked):
//ConversationListFragment.java
@Override
public void onConversationClick(Conversation conversation) {
if (actionMode == null) {
handleCreateConversation(conversation.getThreadRecord().getThreadId(), conversation.getThreadRecord().getRecipient(), conversation.getThreadRecord().getDistributionType());
} else {
defaultAdapter.toggleConversationInBatchSet(conversation);
if (defaultAdapter.getBatchSelectionIds().size() == 0) {
actionMode.finish();
} else {
actionMode.setTitle(String.valueOf(defaultAdapter.getBatchSelectionIds().size()));
setCorrectMenuVisibility(actionMode.getMenu());
}
}
}
进入的是ConversationFragment页面,ConversationFragment属于ConversationActivity。
private void handleCreateConversation(long threadId, Recipient recipient, int distributionType) {
getNavigator().goToConversation(recipient.getId(), threadId, distributionType, -1);
}
public void goToConversation(@NonNull RecipientId recipientId, long threadId, int distributionType, int startingPosition) {
Intent intent = ConversationActivity.buildIntent(activity, recipientId, threadId, distributionType, startingPosition);
activity.startActivity(intent);
activity.overridePendingTransition(R.anim.slide_from_end, R.anim.fade_scale_out);
}
顶部的actionbar
搜索栏的左侧图标是AppCompatImageButton
右侧的选项菜单是ActionMenuView
这里有两个toolbar,默认是toolbar:DarkOverflowToolbar
点击搜索按钮显示的页面时是search_toolbar:SearchToolbar
点击首页的左上角进入的页面
ApplicationPreferencesActivity
点击首页顶部的搜索按钮进入的页面
还是在会话列表页ConversationListFragment.java,只是数据是过滤后的。
搜索按钮是searchToolbar,
//ConversationListFragment.java
private void initializeSearchListener() {
searchAction.setOnClickListener(v -> {
searchToolbar.display(searchAction.getX() + (searchAction.getWidth() / 2.0f),
searchAction.getY() + (searchAction.getHeight() / 2.0f));
});
searchToolbar.setListener(new SearchToolbar.SearchListener() {
@Override
public void onSearchTextChange(String text) {
String trimmed = text.trim();
viewModel.updateQuery(trimmed);
if (trimmed.length() > 0) {
if (activeAdapter != searchAdapter) {
setAdapter(searchAdapter);
list.removeItemDecoration(searchAdapterDecoration);
list.addItemDecoration(searchAdapterDecoration);
}
} else {
if (activeAdapter != defaultAdapter) {
list.removeItemDecoration(searchAdapterDecoration);
setAdapter(defaultAdapter);
}
}
}
@Override
public void onSearchClosed() {
list.removeItemDecoration(searchAdapterDecoration);
setAdapter(defaultAdapter);
}
});
}
点击首页右下角的写短信浮动按钮进入的页面
进入的是联系人页面
这个按钮的点击事件:
//ConversationListFragment.java
fab.setOnClickListener(v -> startActivity(new Intent(getActivity(), NewConversationActivity.class)));
cameraFab.setOnClickListener(v -> {
Permissions.with(requireActivity())
.request(Manifest.permission.CAMERA)
.ifNecessary()
.withRationaleDialog(getString(R.string.ConversationActivity_to_capture_photos_and_video_allow_signal_access_to_the_camera), R.drawable.ic_camera_solid_24)
.withPermanentDenialDialog(getString(R.string.ConversationActivity_signal_needs_the_camera_permission_to_take_photos_or_video))
.onAllGranted(() -> startActivity(MediaSendActivity.buildCameraFirstIntent(requireActivity())))
.onAnyDenied(() -> Toast.makeText(requireContext(), R.string.ConversationActivity_signal_needs_camera_permissions_to_take_photos_or_video, Toast.LENGTH_LONG).show())
.execute();
});
启动了一个新的Activity:NewConversationActivity:
/**
* Activity container for starting a new conversation.
*
* @author Moxie Marlinspike
*
*/
public class NewConversationActivity extends ContactSelectionActivity
implements ContactSelectionListFragment.ListCallback
{
...
}
NewConversationActivity ,继承自ContactSelectionActivity ,页面是ContactSelectionListFragment,布局文件:contact_selection_activity.xml:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout android:layout_gravity="center"
android:layout_height="fill_parent"
android:layout_width="fill_parent"
android:orientation="vertical"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<org.thoughtcrime.securesms.components.ContactFilterToolbar
android:id="@+id/toolbar"
android:layout_height="?attr/actionBarSize"
android:layout_width="match_parent"
android:minHeight="?attr/actionBarSize"
android:elevation="4dp"
android:theme="?attr/actionBarStyle"
app:navigationIcon="@drawable/ic_arrow_left_24"
app:contentInsetStartWithNavigation="0dp"/>
<fragment android:id="@+id/contact_selection_list_fragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:name="org.thoughtcrime.securesms.ContactSelectionListFragment" />
</LinearLayout>
该页面的顶部的搜索栏是ContactFilterToolbar,看下顶部的搜索栏的布局:
邀请好友页面
InviteActivity
invite_activity.xml
顶部是Toolbar