commit
1922ebcba2
@ -1,2 +1,4 @@ |
|||||||
/build |
/build |
||||||
app-release.apk |
app-release.apk |
||||||
|
app-google-release.apk |
||||||
|
src/main/res/raw/keystore_tusky_api.bks |
||||||
|
@ -1,55 +0,0 @@ |
|||||||
{ |
|
||||||
"project_info": { |
|
||||||
"project_number": "268851337880", |
|
||||||
"firebase_url": "https://tusky-62772.firebaseio.com", |
|
||||||
"project_id": "tusky-62772", |
|
||||||
"storage_bucket": "tusky-62772.appspot.com" |
|
||||||
}, |
|
||||||
"client": [ |
|
||||||
{ |
|
||||||
"client_info": { |
|
||||||
"mobilesdk_app_id": "1:268851337880:android:fc4111b1d145a00e", |
|
||||||
"android_client_info": { |
|
||||||
"package_name": "com.keylesspalace.tusky" |
|
||||||
} |
|
||||||
}, |
|
||||||
"oauth_client": [ |
|
||||||
{ |
|
||||||
"client_id": "268851337880-eie2ssto2d21bfihn9d1qupcrke8oebf.apps.googleusercontent.com", |
|
||||||
"client_type": 1, |
|
||||||
"android_info": { |
|
||||||
"package_name": "com.keylesspalace.tusky", |
|
||||||
"certificate_hash": "18d196307d6e928e99c2e0bb9818c01c38aff2f9" |
|
||||||
} |
|
||||||
}, |
|
||||||
{ |
|
||||||
"client_id": "268851337880-n19d05m282nirs1fc9kdd5n4of6je4fk.apps.googleusercontent.com", |
|
||||||
"client_type": 3 |
|
||||||
} |
|
||||||
], |
|
||||||
"api_key": [ |
|
||||||
{ |
|
||||||
"current_key": "AIzaSyCbJtSjuk4I3Jy8PdUaO3TaQOXubcOUElo" |
|
||||||
} |
|
||||||
], |
|
||||||
"services": { |
|
||||||
"analytics_service": { |
|
||||||
"status": 1 |
|
||||||
}, |
|
||||||
"appinvite_service": { |
|
||||||
"status": 2, |
|
||||||
"other_platform_oauth_client": [ |
|
||||||
{ |
|
||||||
"client_id": "268851337880-n19d05m282nirs1fc9kdd5n4of6je4fk.apps.googleusercontent.com", |
|
||||||
"client_type": 3 |
|
||||||
} |
|
||||||
] |
|
||||||
}, |
|
||||||
"ads_service": { |
|
||||||
"status": 2 |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
], |
|
||||||
"configuration_version": "1" |
|
||||||
} |
|
@ -1,12 +0,0 @@ |
|||||||
<?xml version="1.0" encoding="utf-8"?> |
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" |
|
||||||
package="com.keylesspalace.tusky"> |
|
||||||
|
|
||||||
<application> |
|
||||||
<service |
|
||||||
android:name=".MessagingService" |
|
||||||
android:enabled="true" |
|
||||||
android:exported="true" /> |
|
||||||
</application> |
|
||||||
|
|
||||||
</manifest> |
|
@ -1,132 +0,0 @@ |
|||||||
/* Copyright 2017 Andrew Dawson |
|
||||||
* |
|
||||||
* This file is a part of Tusky. |
|
||||||
* |
|
||||||
* This program is free software; you can redistribute it and/or modify it under the terms of the |
|
||||||
* GNU General Public License as published by the Free Software Foundation; either version 3 of the |
|
||||||
* License, or (at your option) any later version. |
|
||||||
* |
|
||||||
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even |
|
||||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General |
|
||||||
* Public License for more details. |
|
||||||
* |
|
||||||
* You should have received a copy of the GNU General Public License along with Tusky; if not, |
|
||||||
* see <http://www.gnu.org/licenses>. */
|
|
||||||
|
|
||||||
package com.keylesspalace.tusky; |
|
||||||
|
|
||||||
import android.app.IntentService; |
|
||||||
import android.content.Context; |
|
||||||
import android.content.Intent; |
|
||||||
import android.content.SharedPreferences; |
|
||||||
import android.preference.PreferenceManager; |
|
||||||
import android.text.Spanned; |
|
||||||
import android.util.ArraySet; |
|
||||||
|
|
||||||
import com.google.gson.Gson; |
|
||||||
import com.google.gson.GsonBuilder; |
|
||||||
|
|
||||||
import com.keylesspalace.tusky.entity.Notification; |
|
||||||
|
|
||||||
import java.util.HashSet; |
|
||||||
import java.util.List; |
|
||||||
|
|
||||||
import java.io.IOException; |
|
||||||
import java.util.Set; |
|
||||||
|
|
||||||
import okhttp3.Interceptor; |
|
||||||
import okhttp3.OkHttpClient; |
|
||||||
import okhttp3.Request; |
|
||||||
import retrofit2.Call; |
|
||||||
import retrofit2.Callback; |
|
||||||
import retrofit2.Response; |
|
||||||
import retrofit2.Retrofit; |
|
||||||
import retrofit2.converter.gson.GsonConverterFactory; |
|
||||||
|
|
||||||
public class MessagingService extends IntentService { |
|
||||||
public static final int NOTIFY_ID = 6; // This is an arbitrary number.
|
|
||||||
|
|
||||||
private MastodonAPI mastodonAPI; |
|
||||||
|
|
||||||
public MessagingService() { |
|
||||||
super("Tusky Pull Notification Service"); |
|
||||||
} |
|
||||||
|
|
||||||
@Override |
|
||||||
protected void onHandleIntent(Intent intent) { |
|
||||||
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences( |
|
||||||
getApplicationContext()); |
|
||||||
boolean enabled = preferences.getBoolean("notificationsEnabled", true); |
|
||||||
if (!enabled) { |
|
||||||
return; |
|
||||||
} |
|
||||||
|
|
||||||
createMastodonApi(); |
|
||||||
|
|
||||||
mastodonAPI.notifications(null, null, null).enqueue(new Callback<List<Notification>>() { |
|
||||||
@Override |
|
||||||
public void onResponse(Call<List<Notification>> call, |
|
||||||
Response<List<Notification>> response) { |
|
||||||
if (response.isSuccessful()) { |
|
||||||
onNotificationsReceived(response.body()); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
@Override |
|
||||||
public void onFailure(Call<List<Notification>> call, Throwable t) {} |
|
||||||
}); |
|
||||||
} |
|
||||||
|
|
||||||
private void createMastodonApi() { |
|
||||||
SharedPreferences preferences = getSharedPreferences( |
|
||||||
getString(R.string.preferences_file_key), Context.MODE_PRIVATE); |
|
||||||
final String domain = preferences.getString("domain", null); |
|
||||||
final String accessToken = preferences.getString("accessToken", null); |
|
||||||
|
|
||||||
OkHttpClient okHttpClient = OkHttpUtils.getCompatibleClientBuilder() |
|
||||||
.addInterceptor(new Interceptor() { |
|
||||||
@Override |
|
||||||
public okhttp3.Response intercept(Chain chain) throws IOException { |
|
||||||
Request originalRequest = chain.request(); |
|
||||||
|
|
||||||
Request.Builder builder = originalRequest.newBuilder() |
|
||||||
.header("Authorization", String.format("Bearer %s", accessToken)); |
|
||||||
|
|
||||||
Request newRequest = builder.build(); |
|
||||||
|
|
||||||
return chain.proceed(newRequest); |
|
||||||
} |
|
||||||
}) |
|
||||||
.build(); |
|
||||||
|
|
||||||
Gson gson = new GsonBuilder() |
|
||||||
.registerTypeAdapter(Spanned.class, new SpannedTypeAdapter()) |
|
||||||
.registerTypeAdapter(StringWithEmoji.class, new StringWithEmojiTypeAdapter()) |
|
||||||
.create(); |
|
||||||
|
|
||||||
Retrofit retrofit = new Retrofit.Builder() |
|
||||||
.baseUrl("https://" + domain) |
|
||||||
.client(okHttpClient) |
|
||||||
.addConverterFactory(GsonConverterFactory.create(gson)) |
|
||||||
.build(); |
|
||||||
|
|
||||||
mastodonAPI = retrofit.create(MastodonAPI.class); |
|
||||||
} |
|
||||||
|
|
||||||
private void onNotificationsReceived(List<Notification> notificationList) { |
|
||||||
SharedPreferences notificationsPreferences = getSharedPreferences( |
|
||||||
"Notifications", Context.MODE_PRIVATE); |
|
||||||
Set<String> currentIds = notificationsPreferences.getStringSet( |
|
||||||
"current_ids", new HashSet<String>()); |
|
||||||
for (Notification notification : notificationList) { |
|
||||||
String id = notification.id; |
|
||||||
if (!currentIds.contains(id)) { |
|
||||||
currentIds.add(id); |
|
||||||
NotificationMaker.make(this, NOTIFY_ID, notification); |
|
||||||
} |
|
||||||
} |
|
||||||
notificationsPreferences.edit() |
|
||||||
.putStringSet("current_ids", currentIds) |
|
||||||
.apply(); |
|
||||||
} |
|
||||||
} |
|
@ -1,18 +0,0 @@ |
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" |
|
||||||
package="com.keylesspalace.tusky"> |
|
||||||
<application> |
|
||||||
<meta-data android:name="firebase_analytics_collection_enabled" android:value="false" /> |
|
||||||
|
|
||||||
<service android:name=".MyFirebaseInstanceIdService" android:exported="true"> |
|
||||||
<intent-filter> |
|
||||||
<action android:name="com.google.firebase.INSTANCE_ID_EVENT" /> |
|
||||||
</intent-filter> |
|
||||||
</service> |
|
||||||
|
|
||||||
<service android:name=".MessagingService" android:exported="true"> |
|
||||||
<intent-filter> |
|
||||||
<action android:name="com.google.firebase.MESSAGING_EVENT" /> |
|
||||||
</intent-filter> |
|
||||||
</service> |
|
||||||
</application> |
|
||||||
</manifest> |
|
@ -1,121 +0,0 @@ |
|||||||
/* Copyright 2017 Andrew Dawson |
|
||||||
* |
|
||||||
* This file is a part of Tusky. |
|
||||||
* |
|
||||||
* This program is free software; you can redistribute it and/or modify it under the terms of the |
|
||||||
* GNU General Public License as published by the Free Software Foundation; either version 3 of the |
|
||||||
* License, or (at your option) any later version. |
|
||||||
* |
|
||||||
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even |
|
||||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General |
|
||||||
* Public License for more details. |
|
||||||
* |
|
||||||
* You should have received a copy of the GNU General Public License along with Tusky; if not, |
|
||||||
* see <http://www.gnu.org/licenses>.
|
|
||||||
* |
|
||||||
* If you modify this Program, or any covered work, by linking or combining it with Firebase Cloud |
|
||||||
* Messaging and Firebase Crash Reporting (or a modified version of those libraries), containing |
|
||||||
* parts covered by the Google APIs Terms of Service, the licensors of this Program grant you |
|
||||||
* additional permission to convey the resulting work. */ |
|
||||||
|
|
||||||
package com.keylesspalace.tusky; |
|
||||||
|
|
||||||
import android.content.Context; |
|
||||||
import android.content.SharedPreferences; |
|
||||||
import android.preference.PreferenceManager; |
|
||||||
import android.text.Spanned; |
|
||||||
|
|
||||||
import com.google.firebase.messaging.FirebaseMessagingService; |
|
||||||
import com.google.firebase.messaging.RemoteMessage; |
|
||||||
import com.google.gson.Gson; |
|
||||||
import com.google.gson.GsonBuilder; |
|
||||||
|
|
||||||
import com.keylesspalace.tusky.entity.Notification; |
|
||||||
|
|
||||||
import java.io.IOException; |
|
||||||
|
|
||||||
import okhttp3.Interceptor; |
|
||||||
import okhttp3.OkHttpClient; |
|
||||||
import okhttp3.Request; |
|
||||||
import retrofit2.Call; |
|
||||||
import retrofit2.Callback; |
|
||||||
import retrofit2.Response; |
|
||||||
import retrofit2.Retrofit; |
|
||||||
import retrofit2.converter.gson.GsonConverterFactory; |
|
||||||
|
|
||||||
public class MessagingService extends FirebaseMessagingService { |
|
||||||
private MastodonAPI mastodonAPI; |
|
||||||
private static final String TAG = "MessagingService"; |
|
||||||
public static final int NOTIFY_ID = 666; |
|
||||||
|
|
||||||
@Override |
|
||||||
public void onMessageReceived(RemoteMessage remoteMessage) { |
|
||||||
Log.d(TAG, remoteMessage.getFrom()); |
|
||||||
Log.d(TAG, remoteMessage.toString()); |
|
||||||
|
|
||||||
String notificationId = remoteMessage.getData().get("notification_id"); |
|
||||||
|
|
||||||
if (notificationId == null) { |
|
||||||
Log.e(TAG, "No notification ID in payload!!"); |
|
||||||
return; |
|
||||||
} |
|
||||||
|
|
||||||
Log.d(TAG, notificationId); |
|
||||||
|
|
||||||
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences( |
|
||||||
getApplicationContext()); |
|
||||||
boolean enabled = preferences.getBoolean("notificationsEnabled", true); |
|
||||||
if (!enabled) { |
|
||||||
return; |
|
||||||
} |
|
||||||
|
|
||||||
createMastodonAPI(); |
|
||||||
|
|
||||||
mastodonAPI.notification(notificationId).enqueue(new Callback<Notification>() { |
|
||||||
@Override |
|
||||||
public void onResponse(Call<Notification> call, Response<Notification> response) { |
|
||||||
if (response.isSuccessful()) { |
|
||||||
NotificationMaker.make(MessagingService.this, NOTIFY_ID, response.body()); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
@Override |
|
||||||
public void onFailure(Call<Notification> call, Throwable t) {} |
|
||||||
}); |
|
||||||
} |
|
||||||
|
|
||||||
private void createMastodonAPI() { |
|
||||||
SharedPreferences preferences = getSharedPreferences(getString(R.string.preferences_file_key), Context.MODE_PRIVATE); |
|
||||||
final String domain = preferences.getString("domain", null); |
|
||||||
final String accessToken = preferences.getString("accessToken", null); |
|
||||||
|
|
||||||
OkHttpClient okHttpClient = OkHttpUtils.getCompatibleClientBuilder() |
|
||||||
.addInterceptor(new Interceptor() { |
|
||||||
@Override |
|
||||||
public okhttp3.Response intercept(Chain chain) throws IOException { |
|
||||||
Request originalRequest = chain.request(); |
|
||||||
|
|
||||||
Request.Builder builder = originalRequest.newBuilder() |
|
||||||
.header("Authorization", String.format("Bearer %s", accessToken)); |
|
||||||
|
|
||||||
Request newRequest = builder.build(); |
|
||||||
|
|
||||||
return chain.proceed(newRequest); |
|
||||||
} |
|
||||||
}) |
|
||||||
.build(); |
|
||||||
|
|
||||||
Gson gson = new GsonBuilder() |
|
||||||
.registerTypeAdapter(Spanned.class, new SpannedTypeAdapter()) |
|
||||||
.registerTypeAdapter(StringWithEmoji.class, new StringWithEmojiTypeAdapter()) |
|
||||||
.create(); |
|
||||||
|
|
||||||
Retrofit retrofit = new Retrofit.Builder() |
|
||||||
.baseUrl("https://" + domain) |
|
||||||
.client(okHttpClient) |
|
||||||
.addConverterFactory(GsonConverterFactory.create(gson)) |
|
||||||
.build(); |
|
||||||
|
|
||||||
mastodonAPI = retrofit.create(MastodonAPI.class); |
|
||||||
} |
|
||||||
} |
|
@ -1,83 +0,0 @@ |
|||||||
/* Copyright 2017 Andrew Dawson |
|
||||||
* |
|
||||||
* This file is a part of Tusky. |
|
||||||
* |
|
||||||
* This program is free software; you can redistribute it and/or modify it under the terms of the |
|
||||||
* GNU General Public License as published by the Free Software Foundation; either version 3 of the |
|
||||||
* License, or (at your option) any later version. |
|
||||||
* |
|
||||||
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even |
|
||||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General |
|
||||||
* Public License for more details. |
|
||||||
* |
|
||||||
* You should have received a copy of the GNU General Public License along with Tusky; if not, |
|
||||||
* see <http://www.gnu.org/licenses>.
|
|
||||||
* |
|
||||||
* If you modify this Program, or any covered work, by linking or combining it with Firebase Cloud |
|
||||||
* Messaging and Firebase Crash Reporting (or a modified version of those libraries), containing |
|
||||||
* parts covered by the Google APIs Terms of Service, the licensors of this Program grant you |
|
||||||
* additional permission to convey the resulting work. */ |
|
||||||
|
|
||||||
package com.keylesspalace.tusky; |
|
||||||
|
|
||||||
import android.content.Context; |
|
||||||
import android.content.SharedPreferences; |
|
||||||
|
|
||||||
import com.google.firebase.iid.FirebaseInstanceId; |
|
||||||
import com.google.firebase.iid.FirebaseInstanceIdService; |
|
||||||
|
|
||||||
import okhttp3.ResponseBody; |
|
||||||
import retrofit2.Call; |
|
||||||
import retrofit2.Callback; |
|
||||||
import retrofit2.Response; |
|
||||||
import retrofit2.Retrofit; |
|
||||||
|
|
||||||
public class MyFirebaseInstanceIdService extends FirebaseInstanceIdService { |
|
||||||
private static final String TAG = "com.keylesspalace.tusky.MyFirebaseInstanceIdService"; |
|
||||||
|
|
||||||
private TuskyAPI tuskyAPI; |
|
||||||
|
|
||||||
protected void createTuskyAPI() { |
|
||||||
Retrofit retrofit = new Retrofit.Builder() |
|
||||||
.baseUrl(getString(R.string.tusky_api_url)) |
|
||||||
.client(OkHttpUtils.getCompatibleClient()) |
|
||||||
.build(); |
|
||||||
|
|
||||||
tuskyAPI = retrofit.create(TuskyAPI.class); |
|
||||||
} |
|
||||||
|
|
||||||
@Override |
|
||||||
public void onTokenRefresh() { |
|
||||||
createTuskyAPI(); |
|
||||||
|
|
||||||
String refreshedToken = FirebaseInstanceId.getInstance().getToken(); |
|
||||||
SharedPreferences preferences = getSharedPreferences(getString(R.string.preferences_file_key), Context.MODE_PRIVATE); |
|
||||||
String accessToken = preferences.getString("accessToken", null); |
|
||||||
String domain = preferences.getString("domain", null); |
|
||||||
|
|
||||||
if (accessToken != null && domain != null) { |
|
||||||
tuskyAPI.unregister("https://" + domain, accessToken).enqueue(new Callback<ResponseBody>() { |
|
||||||
@Override |
|
||||||
public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) { |
|
||||||
Log.d(TAG, response.message()); |
|
||||||
} |
|
||||||
|
|
||||||
@Override |
|
||||||
public void onFailure(Call<ResponseBody> call, Throwable t) { |
|
||||||
Log.d(TAG, t.getMessage()); |
|
||||||
} |
|
||||||
}); |
|
||||||
tuskyAPI.register("https://" + domain, accessToken, refreshedToken).enqueue(new Callback<ResponseBody>() { |
|
||||||
@Override |
|
||||||
public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) { |
|
||||||
Log.d(TAG, response.message()); |
|
||||||
} |
|
||||||
|
|
||||||
@Override |
|
||||||
public void onFailure(Call<ResponseBody> call, Throwable t) { |
|
||||||
Log.d(TAG, t.getMessage()); |
|
||||||
} |
|
||||||
}); |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
@ -0,0 +1,108 @@ |
|||||||
|
package com.keylesspalace.tusky; |
||||||
|
|
||||||
|
import android.content.Intent; |
||||||
|
import android.os.Bundle; |
||||||
|
import android.support.design.widget.Snackbar; |
||||||
|
import android.support.v7.app.ActionBar; |
||||||
|
import android.support.v7.widget.Toolbar; |
||||||
|
import android.view.MenuItem; |
||||||
|
import android.view.View; |
||||||
|
import android.widget.Button; |
||||||
|
import android.widget.TextView; |
||||||
|
|
||||||
|
import com.keylesspalace.tusky.entity.Account; |
||||||
|
|
||||||
|
import java.util.List; |
||||||
|
|
||||||
|
import retrofit2.Call; |
||||||
|
import retrofit2.Callback; |
||||||
|
import retrofit2.Response; |
||||||
|
|
||||||
|
public class AboutActivity extends BaseActivity { |
||||||
|
private Button appAccountButton; |
||||||
|
|
||||||
|
@Override |
||||||
|
protected void onCreate(Bundle savedInstanceState) { |
||||||
|
super.onCreate(savedInstanceState); |
||||||
|
setContentView(R.layout.activity_about); |
||||||
|
|
||||||
|
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); |
||||||
|
setSupportActionBar(toolbar); |
||||||
|
ActionBar bar = getSupportActionBar(); |
||||||
|
if (bar != null) { |
||||||
|
bar.setDisplayHomeAsUpEnabled(true); |
||||||
|
bar.setDisplayShowHomeEnabled(true); |
||||||
|
} |
||||||
|
|
||||||
|
TextView versionTextView = (TextView) findViewById(R.id.versionTV); |
||||||
|
String versionName = BuildConfig.VERSION_NAME; |
||||||
|
String versionFormat = getString(R.string.about_application_version); |
||||||
|
versionTextView.setText(String.format(versionFormat, versionName)); |
||||||
|
|
||||||
|
appAccountButton = (Button) findViewById(R.id.tusky_profile_button); |
||||||
|
appAccountButton.setOnClickListener(new View.OnClickListener() { |
||||||
|
@Override |
||||||
|
public void onClick(View v) { |
||||||
|
onAccountButtonClick(); |
||||||
|
} |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
private void onAccountButtonClick() { |
||||||
|
String appAccountId = getPrivatePreferences().getString("appAccountId", null); |
||||||
|
if (appAccountId != null) { |
||||||
|
viewAccount(appAccountId); |
||||||
|
} else { |
||||||
|
searchForAccountThenViewIt(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private void viewAccount(String id) { |
||||||
|
Intent intent = new Intent(this, AccountActivity.class); |
||||||
|
intent.putExtra("id", id); |
||||||
|
startActivity(intent); |
||||||
|
} |
||||||
|
|
||||||
|
private void searchForAccountThenViewIt() { |
||||||
|
Callback<List<Account>> callback = new Callback<List<Account>>() { |
||||||
|
@Override |
||||||
|
public void onResponse(Call<List<Account>> call, Response<List<Account>> response) { |
||||||
|
if (response.isSuccessful()) { |
||||||
|
List<Account> accountList = response.body(); |
||||||
|
if (!accountList.isEmpty()) { |
||||||
|
String id = accountList.get(0).id; |
||||||
|
getPrivatePreferences().edit() |
||||||
|
.putString("appAccountId", id) |
||||||
|
.apply(); |
||||||
|
viewAccount(id); |
||||||
|
} else { |
||||||
|
onSearchFailed(); |
||||||
|
} |
||||||
|
} else { |
||||||
|
onSearchFailed(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void onFailure(Call<List<Account>> call, Throwable t) { |
||||||
|
onSearchFailed(); |
||||||
|
} |
||||||
|
}; |
||||||
|
mastodonAPI.searchAccounts("Tusky@mastodon.social", true, null).enqueue(callback); |
||||||
|
} |
||||||
|
|
||||||
|
private void onSearchFailed() { |
||||||
|
Snackbar.make(appAccountButton, R.string.error_generic, Snackbar.LENGTH_LONG).show(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public boolean onOptionsItemSelected(MenuItem item) { |
||||||
|
switch (item.getItemId()) { |
||||||
|
case android.R.id.home: { |
||||||
|
onBackPressed(); |
||||||
|
return true; |
||||||
|
} |
||||||
|
} |
||||||
|
return super.onOptionsItemSelected(item); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,28 @@ |
|||||||
|
/* Copyright 2017 Andrew Dawson |
||||||
|
* |
||||||
|
* This file is a part of Tusky. |
||||||
|
* |
||||||
|
* This program is free software; you can redistribute it and/or modify it under the terms of the |
||||||
|
* GNU General Public License as published by the Free Software Foundation; either version 3 of the |
||||||
|
* License, or (at your option) any later version. |
||||||
|
* |
||||||
|
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even |
||||||
|
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General |
||||||
|
* Public License for more details. |
||||||
|
* |
||||||
|
* You should have received a copy of the GNU General Public License along with Tusky; if not, |
||||||
|
* see <http://www.gnu.org/licenses>. */
|
||||||
|
|
||||||
|
package com.keylesspalace.tusky.entity; |
||||||
|
|
||||||
|
public class Session { |
||||||
|
public String instanceUrl; |
||||||
|
public String accessToken; |
||||||
|
public String deviceToken; |
||||||
|
|
||||||
|
public Session(String instanceUrl, String accessToken, String deviceToken) { |
||||||
|
this.instanceUrl = instanceUrl; |
||||||
|
this.accessToken = accessToken; |
||||||
|
this.deviceToken = deviceToken; |
||||||
|
} |
||||||
|
} |
@ -1,4 +1,4 @@ |
|||||||
package com.keylesspalace.tusky; |
package com.keylesspalace.tusky.util; |
||||||
import android.content.Context; |
import android.content.Context; |
||||||
import android.content.Intent; |
import android.content.Intent; |
||||||
import android.content.IntentFilter; |
import android.content.IntentFilter; |
@ -0,0 +1,237 @@ |
|||||||
|
package com.keylesspalace.tusky.util; |
||||||
|
|
||||||
|
import android.app.NotificationManager; |
||||||
|
import android.content.Context; |
||||||
|
import android.support.annotation.NonNull; |
||||||
|
import android.text.Spanned; |
||||||
|
|
||||||
|
import com.google.gson.Gson; |
||||||
|
import com.google.gson.GsonBuilder; |
||||||
|
import com.keylesspalace.tusky.R; |
||||||
|
import com.keylesspalace.tusky.entity.Notification; |
||||||
|
import com.keylesspalace.tusky.json.SpannedTypeAdapter; |
||||||
|
import com.keylesspalace.tusky.json.StringWithEmoji; |
||||||
|
import com.keylesspalace.tusky.json.StringWithEmojiTypeAdapter; |
||||||
|
|
||||||
|
import org.eclipse.paho.android.service.MqttAndroidClient; |
||||||
|
import org.eclipse.paho.client.mqttv3.DisconnectedBufferOptions; |
||||||
|
import org.eclipse.paho.client.mqttv3.IMqttActionListener; |
||||||
|
import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken; |
||||||
|
import org.eclipse.paho.client.mqttv3.IMqttToken; |
||||||
|
import org.eclipse.paho.client.mqttv3.MqttCallbackExtended; |
||||||
|
import org.eclipse.paho.client.mqttv3.MqttClient; |
||||||
|
import org.eclipse.paho.client.mqttv3.MqttConnectOptions; |
||||||
|
import org.eclipse.paho.client.mqttv3.MqttException; |
||||||
|
import org.eclipse.paho.client.mqttv3.MqttMessage; |
||||||
|
|
||||||
|
import java.io.InputStream; |
||||||
|
import java.util.ArrayDeque; |
||||||
|
import java.util.ArrayList; |
||||||
|
|
||||||
|
import static android.content.Context.NOTIFICATION_SERVICE; |
||||||
|
|
||||||
|
public class PushNotificationClient { |
||||||
|
private static final String TAG = "PushNotificationClient"; |
||||||
|
private static final int NOTIFY_ID = 666; |
||||||
|
|
||||||
|
private static class QueuedAction { |
||||||
|
enum Type { |
||||||
|
SUBSCRIBE, |
||||||
|
UNSUBSCRIBE, |
||||||
|
DISCONNECT, |
||||||
|
} |
||||||
|
|
||||||
|
Type type; |
||||||
|
String topic; |
||||||
|
|
||||||
|
QueuedAction(Type type) { |
||||||
|
this.type = type; |
||||||
|
} |
||||||
|
|
||||||
|
QueuedAction(Type type, String topic) { |
||||||
|
this.type = type; |
||||||
|
this.topic = topic; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private MqttAndroidClient mqttAndroidClient; |
||||||
|
private ArrayDeque<QueuedAction> queuedActions; |
||||||
|
private ArrayList<String> subscribedTopics; |
||||||
|
|
||||||
|
public PushNotificationClient(final @NonNull Context applicationContext, |
||||||
|
@NonNull String serverUri) { |
||||||
|
queuedActions = new ArrayDeque<>(); |
||||||
|
subscribedTopics = new ArrayList<>(); |
||||||
|
|
||||||
|
// Create the MQTT client.
|
||||||
|
String clientId = MqttClient.generateClientId(); |
||||||
|
mqttAndroidClient = new MqttAndroidClient(applicationContext, serverUri, clientId); |
||||||
|
mqttAndroidClient.setCallback(new MqttCallbackExtended() { |
||||||
|
@Override |
||||||
|
public void connectComplete(boolean reconnect, String serverURI) { |
||||||
|
if (reconnect) { |
||||||
|
flushQueuedActions(); |
||||||
|
for (String topic : subscribedTopics) { |
||||||
|
subscribeToTopic(topic); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void connectionLost(Throwable cause) { |
||||||
|
onConnectionLost(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void messageArrived(String topic, MqttMessage message) throws Exception { |
||||||
|
onMessageReceived(applicationContext, new String(message.getPayload())); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void deliveryComplete(IMqttDeliveryToken token) { |
||||||
|
// This client is read-only, so this is unused.
|
||||||
|
} |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
private void flushQueuedActions() { |
||||||
|
while (!queuedActions.isEmpty()) { |
||||||
|
QueuedAction action = queuedActions.pop(); |
||||||
|
switch (action.type) { |
||||||
|
case SUBSCRIBE: subscribeToTopic(action.topic); break; |
||||||
|
case UNSUBSCRIBE: unsubscribeToTopic(action.topic); break; |
||||||
|
case DISCONNECT: disconnect(); break; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** Connect to the MQTT broker. */ |
||||||
|
public void connect(Context context) { |
||||||
|
MqttConnectOptions options = new MqttConnectOptions(); |
||||||
|
options.setAutomaticReconnect(true); |
||||||
|
options.setCleanSession(false); |
||||||
|
try { |
||||||
|
String password = context.getString(R.string.tusky_api_keystore_password); |
||||||
|
InputStream keystore = context.getResources().openRawResource(R.raw.keystore_tusky_api); |
||||||
|
try { |
||||||
|
options.setSocketFactory(mqttAndroidClient.getSSLSocketFactory(keystore, password)); |
||||||
|
} finally { |
||||||
|
IOUtils.closeQuietly(keystore); |
||||||
|
} |
||||||
|
mqttAndroidClient.connect(options).setActionCallback(new IMqttActionListener() { |
||||||
|
@Override |
||||||
|
public void onSuccess(IMqttToken asyncActionToken) { |
||||||
|
DisconnectedBufferOptions bufferOptions = new DisconnectedBufferOptions(); |
||||||
|
bufferOptions.setBufferEnabled(true); |
||||||
|
bufferOptions.setBufferSize(100); |
||||||
|
bufferOptions.setPersistBuffer(false); |
||||||
|
bufferOptions.setDeleteOldestMessages(false); |
||||||
|
mqttAndroidClient.setBufferOpts(bufferOptions); |
||||||
|
onConnectionSuccess(); |
||||||
|
flushQueuedActions(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void onFailure(IMqttToken asyncActionToken, Throwable exception) { |
||||||
|
Log.e(TAG, "An exception occurred while connecting. " + exception.getMessage() |
||||||
|
+ " " + exception.getCause()); |
||||||
|
onConnectionFailure(); |
||||||
|
} |
||||||
|
}); |
||||||
|
} catch (MqttException e) { |
||||||
|
Log.e(TAG, "An exception occurred while connecpting. " + e.getMessage()); |
||||||
|
onConnectionFailure(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private void onConnectionSuccess() { |
||||||
|
Log.v(TAG, "The connection succeeded."); |
||||||
|
} |
||||||
|
|
||||||
|
private void onConnectionFailure() { |
||||||
|
Log.v(TAG, "The connection failed."); |
||||||
|
} |
||||||
|
|
||||||
|
private void onConnectionLost() { |
||||||
|
Log.v(TAG, "The connection was lost."); |
||||||
|
} |
||||||
|
|
||||||
|
/** Disconnect from the MQTT broker. */ |
||||||
|
public void disconnect() { |
||||||
|
if (!mqttAndroidClient.isConnected()) { |
||||||
|
queuedActions.add(new QueuedAction(QueuedAction.Type.DISCONNECT)); |
||||||
|
return; |
||||||
|
} |
||||||
|
try { |
||||||
|
mqttAndroidClient.disconnect(); |
||||||
|
} catch (MqttException ex) { |
||||||
|
Log.e(TAG, "An exception occurred while disconnecting."); |
||||||
|
onDisconnectFailed(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private void onDisconnectFailed() { |
||||||
|
Log.v(TAG, "Failed while disconnecting from the broker."); |
||||||
|
} |
||||||
|
|
||||||
|
/** Subscribe to the push notification topic. */ |
||||||
|
public void subscribeToTopic(final String topic) { |
||||||
|
if (!mqttAndroidClient.isConnected()) { |
||||||
|
queuedActions.add(new QueuedAction(QueuedAction.Type.SUBSCRIBE, topic)); |
||||||
|
return; |
||||||
|
} |
||||||
|
try { |
||||||
|
mqttAndroidClient.subscribe(topic, 0, null, new IMqttActionListener() { |
||||||
|
@Override |
||||||
|
public void onSuccess(IMqttToken asyncActionToken) { |
||||||
|
subscribedTopics.add(topic); |
||||||
|
onConnectionSuccess(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void onFailure(IMqttToken asyncActionToken, Throwable exception) { |
||||||
|
Log.e(TAG, "An exception occurred while subscribing." + exception.getMessage()); |
||||||
|
onConnectionFailure(); |
||||||
|
} |
||||||
|
}); |
||||||
|
} catch (MqttException e) { |
||||||
|
Log.e(TAG, "An exception occurred while subscribing." + e.getMessage()); |
||||||
|
onConnectionFailure(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** Unsubscribe from the push notification topic. */ |
||||||
|
public void unsubscribeToTopic(String topic) { |
||||||
|
if (!mqttAndroidClient.isConnected()) { |
||||||
|
queuedActions.add(new QueuedAction(QueuedAction.Type.UNSUBSCRIBE, topic)); |
||||||
|
return; |
||||||
|
} |
||||||
|
try { |
||||||
|
mqttAndroidClient.unsubscribe(topic); |
||||||
|
subscribedTopics.remove(topic); |
||||||
|
} catch (MqttException e) { |
||||||
|
Log.e(TAG, "An exception occurred while unsubscribing." + e.getMessage()); |
||||||
|
onConnectionFailure(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private void onMessageReceived(final Context context, String message) { |
||||||
|
Log.v(TAG, "Notification received: " + message); |
||||||
|
|
||||||
|
Gson gson = new GsonBuilder() |
||||||
|
.registerTypeAdapter(Spanned.class, new SpannedTypeAdapter()) |
||||||
|
.registerTypeAdapter(StringWithEmoji.class, new StringWithEmojiTypeAdapter()) |
||||||
|
.create(); |
||||||
|
Notification notification = gson.fromJson(message, Notification.class); |
||||||
|
|
||||||
|
NotificationMaker.make(context, NOTIFY_ID, notification); |
||||||
|
} |
||||||
|
|
||||||
|
public void clearNotifications(Context context) { |
||||||
|
((NotificationManager) (context.getSystemService(NOTIFICATION_SERVICE))).cancel(NOTIFY_ID); |
||||||
|
} |
||||||
|
|
||||||
|
public String getDeviceToken() { |
||||||
|
return mqttAndroidClient.getClientId(); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,41 @@ |
|||||||
|
package com.keylesspalace.tusky.util; |
||||||
|
|
||||||
|
import android.content.BroadcastReceiver; |
||||||
|
import android.content.Context; |
||||||
|
import android.content.Intent; |
||||||
|
import android.content.IntentFilter; |
||||||
|
|
||||||
|
import com.keylesspalace.tusky.adapter.TimelineAdapter; |
||||||
|
import com.keylesspalace.tusky.fragment.TimelineFragment; |
||||||
|
|
||||||
|
public class TimelineReceiver extends BroadcastReceiver { |
||||||
|
public static final class Types { |
||||||
|
public static final String UNFOLLOW_ACCOUNT = "UNFOLLOW_ACCOUNT"; |
||||||
|
public static final String BLOCK_ACCOUNT = "BLOCK_ACCOUNT"; |
||||||
|
public static final String MUTE_ACCOUNT = "MUTE_ACCOUNT"; |
||||||
|
} |
||||||
|
|
||||||
|
TimelineAdapter adapter; |
||||||
|
|
||||||
|
public TimelineReceiver(TimelineAdapter adapter) { |
||||||
|
super(); |
||||||
|
this.adapter = adapter; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void onReceive(Context context, final Intent intent) { |
||||||
|
String id = intent.getStringExtra("id"); |
||||||
|
adapter.removeAllByAccountId(id); |
||||||
|
} |
||||||
|
|
||||||
|
public static IntentFilter getFilter(TimelineFragment.Kind kind) { |
||||||
|
IntentFilter intentFilter = new IntentFilter(); |
||||||
|
if (kind == TimelineFragment.Kind.HOME) { |
||||||
|
intentFilter.addAction(Types.UNFOLLOW_ACCOUNT); |
||||||
|
} |
||||||
|
intentFilter.addAction(Types.BLOCK_ACCOUNT); |
||||||
|
intentFilter.addAction(Types.MUTE_ACCOUNT); |
||||||
|
|
||||||
|
return intentFilter; |
||||||
|
} |
||||||
|
} |
Before Width: | Height: | Size: 1.8 KiB |
@ -0,0 +1,14 @@ |
|||||||
|
<?xml version="1.0" encoding="utf-8"?> |
||||||
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android"> |
||||||
|
<item> |
||||||
|
<bitmap |
||||||
|
android:src="@drawable/splash_pattern" |
||||||
|
android:tileMode="repeat" /> |
||||||
|
</item> |
||||||
|
<item> |
||||||
|
<bitmap |
||||||
|
android:src="@mipmap/ic_logo" |
||||||
|
android:gravity="center" |
||||||
|
android:tileMode="disabled" /> |
||||||
|
</item> |
||||||
|
</layer-list> |
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 40 KiB |
@ -0,0 +1,9 @@ |
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android" |
||||||
|
android:width="24dp" |
||||||
|
android:height="24dp" |
||||||
|
android:viewportWidth="24.0" |
||||||
|
android:viewportHeight="24.0"> |
||||||
|
<path |
||||||
|
android:fillColor="#FFF" |
||||||
|
android:pathData="M19,9h-4V3H9v6H5l7,7 7,-7zM5,18v2h14v-2H5z"/> |
||||||
|
</vector> |
After Width: | Height: | Size: 2.6 KiB |
@ -0,0 +1,92 @@ |
|||||||
|
<?xml version="1.0" encoding="utf-8"?> |
||||||
|
<android.support.design.widget.CoordinatorLayout 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:layout_width="match_parent" |
||||||
|
android:layout_height="match_parent" |
||||||
|
tools:context="com.keylesspalace.tusky.AboutActivity"> |
||||||
|
|
||||||
|
<LinearLayout |
||||||
|
android:layout_width="match_parent" |
||||||
|
android:layout_height="match_parent" |
||||||
|
android:orientation="vertical"> |
||||||
|
|
||||||
|
<android.support.v7.widget.Toolbar |
||||||
|
android:id="@+id/toolbar" |
||||||
|
android:layout_width="match_parent" |
||||||
|
android:layout_height="?attr/actionBarSize" |
||||||
|
android:background="?attr/toolbar_background_color" |
||||||
|
android:elevation="4dp" /> |
||||||
|
|
||||||
|
<ScrollView |
||||||
|
android:layout_width="match_parent" |
||||||
|
android:layout_height="wrap_content"> |
||||||
|
|
||||||
|
<LinearLayout |
||||||
|
android:layout_width="match_parent" |
||||||
|
android:layout_height="wrap_content" |
||||||
|
android:orientation="vertical" |
||||||
|
android:gravity="center" |
||||||
|
android:paddingTop="16dp" |
||||||
|
android:paddingBottom="16dp"> |
||||||
|
|
||||||
|
<com.mikhaellopez.circularfillableloaders.CircularFillableLoaders |
||||||
|
android:id="@+id/circularFillableLoaders" |
||||||
|
android:layout_width="200dp" |
||||||
|
android:layout_height="200dp" |
||||||
|
android:src="@mipmap/ic_logo" |
||||||
|
app:cfl_border="true" |
||||||
|
app:cfl_border_width="4dp" |
||||||
|
app:cfl_progress="80" |
||||||
|
app:cfl_wave_amplitude="0.08" |
||||||
|
app:cfl_wave_color="?attr/splash_wave_color" /> |
||||||
|
|
||||||
|
<TextView |
||||||
|
android:id="@+id/versionTV" |
||||||
|
android:layout_width="wrap_content" |
||||||
|
android:layout_height="wrap_content" |
||||||
|
android:paddingTop="8dp" |
||||||
|
android:textIsSelectable="true" |
||||||
|
android:textAppearance="@style/TextAppearance.AppCompat.Large" /> |
||||||
|
|
||||||
|
<TextView |
||||||
|
android:id="@+id/projectURL_TV" |
||||||
|
android:layout_width="wrap_content" |
||||||
|
android:layout_height="wrap_content" |
||||||
|
android:autoLink="web" |
||||||
|
android:textIsSelectable="true" |
||||||
|
android:padding="@dimen/text_content_margin" |
||||||
|
android:text="@string/about_project_site" |
||||||
|
android:textAlignment="center" |
||||||
|
android:textAppearance="@style/TextAppearance.AppCompat.Medium" /> |
||||||
|
|
||||||
|
<TextView |
||||||
|
android:id="@+id/featuresURL_TV" |
||||||
|
android:layout_width="wrap_content" |
||||||
|
android:layout_height="wrap_content" |
||||||
|
android:autoLink="web" |
||||||
|
android:textIsSelectable="true" |
||||||
|
android:padding="@dimen/text_content_margin" |
||||||
|
android:text="@string/about_bug_feature_request_site" |
||||||
|
android:textAlignment="center" |
||||||
|
android:textAppearance="@style/TextAppearance.AppCompat.Medium" /> |
||||||
|
|
||||||
|
<Button |
||||||
|
android:id="@+id/tusky_profile_button" |
||||||
|
android:layout_width="wrap_content" |
||||||
|
android:layout_height="wrap_content" |
||||||
|
android:clickable="true" |
||||||
|
android:padding="@dimen/text_content_margin" |
||||||
|
android:text="@string/about_tusky_account" |
||||||
|
android:textAlignment="center" |
||||||
|
android:textAllCaps="false" |
||||||
|
android:textAppearance="@style/TextAppearance.AppCompat.Medium" |
||||||
|
android:textColor="@android:color/white" /> |
||||||
|
|
||||||
|
</LinearLayout> |
||||||
|
|
||||||
|
</ScrollView> |
||||||
|
|
||||||
|
</LinearLayout> |
||||||
|
|
||||||
|
</android.support.design.widget.CoordinatorLayout> |
@ -1,19 +0,0 @@ |
|||||||
<?xml version="1.0" encoding="utf-8"?> |
|
||||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" |
|
||||||
android:layout_width="match_parent" |
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto" |
|
||||||
android:gravity="center" |
|
||||||
android:orientation="vertical" |
|
||||||
android:background="?attr/splash_background_color" |
|
||||||
android:layout_height="match_parent"> |
|
||||||
<com.mikhaellopez.circularfillableloaders.CircularFillableLoaders |
|
||||||
android:id="@+id/circularFillableLoaders" |
|
||||||
android:layout_width="200dp" |
|
||||||
android:layout_height="200dp" |
|
||||||
android:src="@mipmap/ic_logo" |
|
||||||
app:cfl_border="true" |
|
||||||
app:cfl_border_width="4dp" |
|
||||||
app:cfl_progress="80" |
|
||||||
app:cfl_wave_amplitude="0.08" |
|
||||||
app:cfl_wave_color="?attr/splash_wave_color" /> |
|
||||||
</LinearLayout> |
|
@ -1,12 +1,30 @@ |
|||||||
<?xml version="1.0" encoding="utf-8"?> |
<?xml version="1.0" encoding="utf-8"?> |
||||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" |
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" |
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto" |
||||||
android:layout_width="match_parent" |
android:layout_width="match_parent" |
||||||
android:layout_height="match_parent" |
android:layout_height="match_parent" |
||||||
android:clickable="true" |
|
||||||
android:layout_gravity="center" |
android:layout_gravity="center" |
||||||
android:background="@android:color/black"> |
android:background="@android:color/black" |
||||||
<uk.co.senab.photoview.PhotoView |
android:clickable="true"> |
||||||
|
|
||||||
|
<com.github.chrisbanes.photoview.PhotoView |
||||||
android:id="@+id/view_media_image" |
android:id="@+id/view_media_image" |
||||||
android:layout_width="match_parent" |
android:layout_width="match_parent" |
||||||
android:layout_height="match_parent" /> |
android:layout_height="match_parent" /> |
||||||
</FrameLayout> |
|
||||||
|
<android.support.v7.widget.Toolbar |
||||||
|
android:id="@+id/toolbar" |
||||||
|
android:layout_width="match_parent" |
||||||
|
android:layout_height="?attr/actionBarSize" |
||||||
|
android:background="@color/toolbar_view_media" |
||||||
|
app:navigationIcon="?attr/homeAsUpIndicator" /> |
||||||
|
|
||||||
|
<ProgressBar |
||||||
|
android:id="@+id/view_media_progress" |
||||||
|
android:layout_width="wrap_content" |
||||||
|
android:layout_height="wrap_content" |
||||||
|
android:layout_centerHorizontal="true" |
||||||
|
android:layout_centerVertical="true" |
||||||
|
android:layout_gravity="center" /> |
||||||
|
|
||||||
|
</RelativeLayout> |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue