├── .gitignore ├── README.md ├── build.gradle ├── example ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── org │ │ └── chatsecure │ │ └── pushdemo │ │ └── ApplicationTest.java │ └── main │ ├── AndroidManifest.xml │ ├── ic_launcher-web.png │ ├── java │ └── org │ │ └── chatsecure │ │ └── pushdemo │ │ ├── Application.java │ │ ├── DataProvider.java │ │ ├── MainActivity.java │ │ ├── Registration.java │ │ ├── gcm │ │ ├── GcmService.java │ │ ├── MyInstanceIDListenerService.java │ │ └── RegistrationIntentService.java │ │ └── ui │ │ ├── adapter │ │ ├── DeviceAdapter.java │ │ └── TokenAdapter.java │ │ └── fragment │ │ ├── DevicesFragment.java │ │ ├── MessagingFragment.java │ │ ├── RegistrationFragment.java │ │ └── TokensFragment.java │ └── res │ ├── drawable-hdpi-v11 │ └── ic_block.png │ ├── drawable-hdpi-v9 │ └── ic_block.png │ ├── drawable-hdpi │ └── ic_block.png │ ├── drawable-mdpi-v11 │ └── ic_block.png │ ├── drawable-mdpi-v9 │ └── ic_block.png │ ├── drawable-mdpi │ └── ic_block.png │ ├── drawable-xhdpi-v11 │ └── ic_block.png │ ├── drawable-xhdpi-v9 │ └── ic_block.png │ ├── drawable-xhdpi │ └── ic_block.png │ ├── drawable-xxhdpi-v11 │ └── ic_block.png │ ├── drawable-xxhdpi-v9 │ └── ic_block.png │ ├── drawable-xxhdpi │ ├── ic_block.png │ └── ic_menu_black_24dp.png │ ├── layout │ ├── activity_main.xml │ ├── device_item.xml │ ├── drawer_header.xml │ ├── fragment_messaging.xml │ ├── fragment_push_secure_registration.xml │ ├── recyclerview.xml │ └── token_item.xml │ ├── menu │ └── drawer.xml │ ├── mipmap-hdpi │ └── ic_launcher.png │ ├── mipmap-mdpi │ └── ic_launcher.png │ ├── mipmap-xhdpi │ └── ic_launcher.png │ ├── mipmap-xxhdpi │ └── ic_launcher.png │ ├── mipmap-xxxhdpi │ └── ic_launcher.png │ ├── values-w820dp │ └── dimens.xml │ └── values │ ├── dimens.xml │ ├── strings.xml │ └── styles.xml ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── sdk ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── org │ │ └── chatsecure │ │ └── pushsecure │ │ └── ApplicationTest.java │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── org │ │ └── chatsecure │ │ └── pushsecure │ │ ├── PushSecureApi.java │ │ ├── PushSecureClient.java │ │ ├── gcm │ │ ├── PushMessage.java │ │ └── PushParser.java │ │ └── response │ │ ├── Account.java │ │ ├── Device.java │ │ ├── DeviceList.java │ │ ├── List.java │ │ ├── Message.java │ │ ├── PushToken.java │ │ ├── TokenList.java │ │ └── typeadapter │ │ └── DjangoDateTypeAdapter.java │ └── res │ └── values │ └── strings.xml └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | google-services.json 2 | .gradle 3 | local.properties 4 | .idea 5 | *.iml 6 | .DS_Store 7 | build 8 | captures 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #PushSecureDemo-Android 2 | 3 | This is a demo [ChatSecure Push Server](https://github.com/ChatSecure/ChatSecure-Push-Server) Android client. 4 | 5 | # Requirements 6 | 7 | This Android demo requires a **ChatSecure Push Server** and a **Google Cloud Messaging account**. 8 | 9 | 1. Clone and setup the [ChatSecure Push Server](https://github.com/ChatSecure/ChatSecure-Push-Server) Django project. You can also test against our demo heroku instance at `https://chatsecure-push.herokuapp.com/api/v1/`. 10 | 11 | 2. Register a Google Cloud Messaging Application with [Google Developers](https://developers.google.com/mobile/add) 12 | 13 | At the conclusion of the registration process you'll be presented with a `Server API Key` and a `google-services.json` file. 14 | 15 | 3. Copy the GCM `Server API Key` to `./push/push/local_settings.py` in the ChatSecure Push Server Django project. Copy `google-services.json` to this project's `./example` directory. 16 | 17 | ## Using the SDK 18 | 19 | Currently, you must include ChatSecure Push as a submodule. Releases will be published as Maven artifacts. 20 | 21 | ### 1. Get the SDK 22 | 23 | Add this repository as a git submodule: 24 | 25 | ``` 26 | $ cd your/project/root 27 | $ git submodule add https://github.com/ChatSecure/ChatSecure-Push-Android.git ./submodules/chatsecure-push/ 28 | ``` 29 | 30 | Edit your project's root `settings.gradle`. This makes the PushSecure submodule's gradle module available to any other gradle modules within your project. 31 | 32 | ```groovy 33 | include ':myapp', ':submodules:chatsecure-push:sdk' 34 | ``` 35 | 36 | Edit your application module's `build.gradle`. This informs gradle that your application's gradle module depends on the PushSecure gradle module (submodule). Say that five times fast. 37 | 38 | ```groovy 39 | ... 40 | dependencies { 41 | compile project(':submodules:chatsecure-push:sdk') 42 | } 43 | ``` 44 | 45 | ### 2. Create a PushSecureClient 46 | 47 | The API client is designed to operate against any compatible ChatSecure-Push backend. 48 | 49 | ```java 50 | PushSecureClient client = new PushSecureClient("https://chatsecure-push.herokuapp.com/api/v1/"); 51 | ``` 52 | 53 | ### 3. Authenticate a user account 54 | 55 | You'll need to have an `Account` registered with the API client to perform requests. 56 | You can create or login to an existing account with the `authenticateAccount` method. 57 | You should generally do this once per app-launch to ensure you have a fresh Account authentication token. 58 | 59 | ```java 60 | client.authenticateAccount(requiredUsername, requiredPassword, optionalEmail, 61 | new RequestCallback() { 62 | @Override 63 | public void onSuccess(Account response) { 64 | // Authenticated Account 65 | // Register this account with the api client 66 | // to perform authenticated requests 67 | client.setAccount(response); 68 | } 69 | 70 | @Override 71 | public void onFailure(Throwable throwable) { 72 | // An error occurred 73 | } 74 | }); 75 | ``` 76 | 77 | If you have a persisted `Account` object you can set that at any time. This might be useful if you're managing multiple `Account`s from a single device. 78 | 79 | ```java 80 | client.setAccount(account); 81 | ``` 82 | 83 | ### 4. Register a Pushable Device 84 | 85 | On Android, we'll obtain a GCM token and register a GCM device with ChatSecure Push. 86 | 87 | ```java 88 | // Retrieve your GCM token as requiredGcmToken 89 | // See [Google's example](https://github.com/googlesamples/google-services/blob/e06754fc7d0e4bf856c001a82fb630abd1b9492a/android/gcm/app/src/main/java/gcm/play/android/samples/com/gcmquickstart/RegistrationIntentService.java#L54) 90 | client.createDevice(requiredGcmToken, optionalName, optionalDeviceId, 91 | new RequestCallback() { 92 | @Override 93 | public void onSuccess(Device response) { 94 | // Registered Device 95 | } 96 | 97 | @Override 98 | public void onFailure(Throwable throwable) { 99 | // An error occurred 100 | } 101 | }); 102 | ``` 103 | 104 | ### 5. Request a Whitelist Token 105 | 106 | A Whitelist token gives its bearer push access to your device. It can be revoked at any time (see 5a). 107 | 108 | ```java 109 | client.createToken(requiredDevice, optionalName, 110 | new RequestCallback() { 111 | @Override 112 | public void onSuccess(PushToken response) { 113 | // Created push token. Share this with a pal 114 | // to let them send your device push messages! 115 | } 116 | 117 | @Override 118 | public void onFailure(Throwable throwable) { 119 | // An error occurred 120 | } 121 | }); 122 | ``` 123 | 124 | ### 5a. Revoke a Whitelist token 125 | 126 | This removes its affiliation with your device, and you will no longer receive pushes from uers who "know you" by this token. 127 | 128 | Holders of the revoked token won't be immediately notified, but they will be told their token "does not exist" the next time they try to use it. 129 | 130 | ```java 131 | client.deleteToken(requiredTokenString, 132 | new RequestCallback() { 133 | @Override 134 | public void onSuccess(Void response) { 135 | // Revoked push token 136 | } 137 | 138 | @Override 139 | public void onFailure(Throwable throwable) { 140 | // An error occurred 141 | } 142 | }); 143 | ``` 144 | 145 | ### 6. Send a Push Message 146 | 147 | Push Message Recipients are always identified by their Whitelist token. 148 | 149 | ```java 150 | client.sendMessage(requiredWhitelistTokenString, optionalData, 151 | new RequestCallback() { 152 | @Override 153 | public void onSuccess(Message response) { 154 | // Sent message 155 | } 156 | 157 | @Override 158 | public void onFailure(Throwable throwable) { 159 | // An error occurred 160 | } 161 | }); 162 | ``` 163 | 164 | ### 7. Parse incoming ChatSecure Push GCM Messages 165 | 166 | See [Google's Example](https://github.com/googlesamples/google-services/blob/e06754fc7d0e4bf856c001a82fb630abd1b9492a/android/gcm/app/src/main/java/gcm/play/android/samples/com/gcmquickstart/MyGcmListenerService.java) for a complete `GcmListenerService` implementation. Below we include the additions necessary to parse ChatSecure Push messages. 167 | 168 | ```java 169 | 170 | public class MyGcmService extends GcmListenerService { 171 | 172 | PushParser parser = new PushParser(); 173 | 174 | @Override 175 | public void onMessageReceived(String from, Bundle data) { 176 | 177 | PushMessage push = parser.parseBundle(from, data); 178 | 179 | if (push != null) 180 | Log.d("GotPush", "Received '" + push.payload + "' via token: " + push.token); 181 | } 182 | ... 183 | } 184 | ``` 185 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | repositories { 5 | jcenter() 6 | } 7 | dependencies { 8 | classpath 'com.android.tools.build:gradle:1.3.0' 9 | classpath 'com.google.gms:google-services:1.3.0-beta1' 10 | classpath 'me.tatarka:gradle-retrolambda:3.1.0' 11 | 12 | 13 | // NOTE: Do not place your application dependencies here; they belong 14 | // in the individual module build.gradle files 15 | } 16 | } 17 | 18 | allprojects { 19 | repositories { 20 | jcenter() 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /example/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | apply plugin: 'com.google.gms.google-services' 3 | apply plugin: 'me.tatarka.retrolambda' 4 | 5 | android { 6 | compileSdkVersion 23 7 | buildToolsVersion "22.0.1" 8 | 9 | defaultConfig { 10 | applicationId "org.chatsecure.pushdemo" 11 | minSdkVersion 15 12 | targetSdkVersion 23 13 | versionCode 1 14 | versionName "1.0" 15 | } 16 | buildTypes { 17 | release { 18 | minifyEnabled false 19 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 20 | } 21 | } 22 | compileOptions { 23 | sourceCompatibility JavaVersion.VERSION_1_8 24 | targetCompatibility JavaVersion.VERSION_1_8 25 | } 26 | } 27 | 28 | dependencies { 29 | 30 | compile project(':sdk') 31 | 32 | // Android support libraries 33 | compile 'com.android.support:design:23.0.1' 34 | compile 'com.android.support:support-v4:23.0.1' 35 | compile 'com.android.support:appcompat-v7:23.0.1' 36 | compile 'com.android.support:recyclerview-v7:23.0.1' 37 | compile 'com.android.support:cardview-v7:23.0.1' 38 | 39 | // Google Cloud Messaging 40 | compile 'com.google.android.gms:play-services-gcm:7.8.0' 41 | 42 | // 3rd party 43 | compile 'com.jakewharton.timber:timber:3.1.0' 44 | compile 'com.jakewharton:butterknife:7.0.1' 45 | compile 'io.reactivex:rxandroid:1.0.1' 46 | compile 'com.jakewharton.rxbinding:rxbinding:0.2.0' 47 | } 48 | -------------------------------------------------------------------------------- /example/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /Users/davidbrodsky/sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /example/src/androidTest/java/org/chatsecure/pushdemo/ApplicationTest.java: -------------------------------------------------------------------------------- 1 | package org.chatsecure.pushdemo; 2 | 3 | import android.app.Application; 4 | import android.test.ApplicationTestCase; 5 | 6 | /** 7 | * Testing Fundamentals 8 | */ 9 | public class ApplicationTest extends ApplicationTestCase { 10 | public ApplicationTest() { 11 | super(Application.class); 12 | } 13 | } -------------------------------------------------------------------------------- /example/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 12 | 13 | 14 | 15 | 16 | 23 | 24 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 49 | 50 | 51 | 52 | 53 | 54 | 57 | 58 | 59 | 60 | 61 | 62 | 65 | 66 | 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /example/src/main/ic_launcher-web.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChatSecure/ChatSecure-Push-Android/2cf8f066bcf2ae062a84d63bdb324a89a3b9534c/example/src/main/ic_launcher-web.png -------------------------------------------------------------------------------- /example/src/main/java/org/chatsecure/pushdemo/Application.java: -------------------------------------------------------------------------------- 1 | package org.chatsecure.pushdemo; 2 | 3 | import timber.log.Timber; 4 | 5 | /** 6 | * Created by davidbrodsky on 6/23/15. 7 | */ 8 | public class Application extends android.app.Application { 9 | 10 | @Override 11 | public void onCreate() { 12 | super.onCreate(); 13 | 14 | if (BuildConfig.DEBUG) { 15 | Timber.plant(new Timber.DebugTree()); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /example/src/main/java/org/chatsecure/pushdemo/DataProvider.java: -------------------------------------------------------------------------------- 1 | package org.chatsecure.pushdemo; 2 | 3 | import android.content.Context; 4 | import android.content.SharedPreferences; 5 | import android.support.annotation.NonNull; 6 | import android.support.annotation.Nullable; 7 | 8 | import org.chatsecure.pushsecure.response.Device; 9 | 10 | import java.text.ParseException; 11 | import java.text.SimpleDateFormat; 12 | 13 | import timber.log.Timber; 14 | 15 | /** 16 | * Persists application data using {@link SharedPreferences} 17 | * 18 | * Created by davidbrodsky on 7/7/15. 19 | */ 20 | public class DataProvider { 21 | 22 | private static final String SENT_GCM_TOKEN_TO_SERVER = "sentGcmTokenToServer"; // boolean 23 | private static final String REGISTRATION_COMPLETE = "registrationComplete"; // boolean 24 | private static final String PUSHSECURE_UNAME = "psUsername"; // String 25 | private static final String PUSHSECURE_TOKEN = "psToken"; // String 26 | 27 | // This device storage 28 | private static final String GCM_TOKEN = "gcmToken"; // String 29 | private static final String DEVICE_SERVER_ID = "deviceSId"; // String 30 | private static final String DEVICE_ID = "deviceId"; // String 31 | private static final String DEVICE_NAME = "deviceName"; // String 32 | private static final String DEVICE_ACTIVE = "deviceActive"; // boolean 33 | private static final String DEVICE_CREATION_DATE = "deviceCreationDate"; // String 34 | 35 | private static final String SHARED_PREFS_NAME = DataProvider.class.getSimpleName(); 36 | 37 | private SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSSZ"); 38 | private SharedPreferences sharedPrefs; 39 | private SharedPreferences.Editor editor; 40 | 41 | public DataProvider(@NonNull Context context) { 42 | sharedPrefs = context.getSharedPreferences(SHARED_PREFS_NAME, Context.MODE_PRIVATE); 43 | editor = sharedPrefs.edit(); 44 | } 45 | 46 | @SuppressWarnings("ConstantConditions") 47 | public @Nullable Device getDevice() { 48 | if (sharedPrefs.getString(GCM_TOKEN, null) == null || 49 | sharedPrefs.getString(DEVICE_SERVER_ID, null) == null) { 50 | 51 | Timber.w("The returned device is not fully registered. Returning null"); 52 | return null; 53 | } 54 | 55 | try { 56 | return new Device(sharedPrefs.getString(DEVICE_NAME, null), 57 | sharedPrefs.getString(GCM_TOKEN, null), 58 | sharedPrefs.getString(DEVICE_ID, null), 59 | sharedPrefs.getString(DEVICE_SERVER_ID, null), 60 | sharedPrefs.getBoolean(DEVICE_ACTIVE, true), 61 | sdf.parse(sharedPrefs.getString(DEVICE_CREATION_DATE, null))); 62 | } catch (ParseException e) { 63 | Timber.e(e, "Unable to restore device from persisted data"); 64 | return null; 65 | } 66 | } 67 | 68 | public void setDevice(@NonNull Device device) { 69 | editor.putString(DEVICE_NAME, device.name) 70 | .putString(GCM_TOKEN, device.registrationId) 71 | .putString(DEVICE_ID, device.deviceId) 72 | .putString(DEVICE_SERVER_ID, device.id) 73 | .putBoolean(DEVICE_ACTIVE, device.active) 74 | .putString(DEVICE_CREATION_DATE, sdf.format(device.dateCreated)).apply(); 75 | } 76 | 77 | public @Nullable String getPushSecureUsername() { 78 | return sharedPrefs.getString(PUSHSECURE_UNAME, null); 79 | } 80 | 81 | public void setPushSecureUsername(@NonNull String username) { 82 | editor.putString(PUSHSECURE_UNAME, username).apply(); 83 | } 84 | 85 | public @Nullable String getPushSecureAuthToken() { 86 | return sharedPrefs.getString(PUSHSECURE_TOKEN, null); 87 | } 88 | 89 | public void setPushSecureAuthToken(@NonNull String newToken) { 90 | editor.putString(PUSHSECURE_TOKEN, newToken).apply(); 91 | } 92 | 93 | public boolean didSendGcmTokenToPushSecure() { 94 | return sharedPrefs.getBoolean(SENT_GCM_TOKEN_TO_SERVER, false); 95 | } 96 | 97 | public void setDidSendGcmTokenToPushSecure(boolean didSend) { 98 | editor.putBoolean(SENT_GCM_TOKEN_TO_SERVER, didSend).apply(); 99 | } 100 | 101 | public void clear() { 102 | editor.clear().apply(); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /example/src/main/java/org/chatsecure/pushdemo/MainActivity.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2015 Google Inc. All Rights Reserved. 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 | 17 | package org.chatsecure.pushdemo; 18 | 19 | import android.app.NotificationManager; 20 | import android.content.Intent; 21 | import android.os.Bundle; 22 | import android.support.design.widget.NavigationView; 23 | import android.support.design.widget.Snackbar; 24 | import android.support.v4.app.Fragment; 25 | import android.support.v4.app.FragmentTransaction; 26 | import android.support.v4.view.GravityCompat; 27 | import android.support.v4.widget.DrawerLayout; 28 | import android.support.v7.app.ActionBar; 29 | import android.support.v7.app.AppCompatActivity; 30 | import android.support.v7.widget.Toolbar; 31 | import android.view.MenuItem; 32 | import android.view.View; 33 | import android.widget.Button; 34 | import android.widget.FrameLayout; 35 | import android.widget.TextView; 36 | 37 | import com.google.android.gms.common.ConnectionResult; 38 | import com.google.android.gms.common.GooglePlayServicesUtil; 39 | 40 | import org.chatsecure.pushdemo.gcm.GcmService; 41 | import org.chatsecure.pushdemo.gcm.RegistrationIntentService; 42 | import org.chatsecure.pushdemo.ui.fragment.DevicesFragment; 43 | import org.chatsecure.pushdemo.ui.fragment.MessagingFragment; 44 | import org.chatsecure.pushdemo.ui.fragment.RegistrationFragment; 45 | import org.chatsecure.pushdemo.ui.fragment.TokensFragment; 46 | import org.chatsecure.pushsecure.PushSecureClient; 47 | import org.chatsecure.pushsecure.response.Account; 48 | 49 | import butterknife.Bind; 50 | import butterknife.ButterKnife; 51 | import retrofit.Callback; 52 | import retrofit.Response; 53 | import rx.android.schedulers.AndroidSchedulers; 54 | import rx.subjects.PublishSubject; 55 | import timber.log.Timber; 56 | 57 | public class MainActivity extends AppCompatActivity implements RegistrationFragment.AccountRegistrationListener, NavigationView.OnNavigationItemSelectedListener, View.OnClickListener { 58 | 59 | private static final int PLAY_SERVICES_RESOLUTION_REQUEST = 9000; 60 | 61 | @Bind(R.id.toolbar) 62 | Toolbar toolbar; 63 | 64 | @Bind(R.id.drawer) 65 | DrawerLayout drawer; 66 | 67 | @Bind(R.id.navigation) 68 | NavigationView navigation; 69 | 70 | @Bind(R.id.container) 71 | FrameLayout container; 72 | 73 | @Bind(R.id.nameTextView) 74 | TextView name; 75 | 76 | @Bind(R.id.signOutButton) 77 | Button signOut; 78 | 79 | private org.chatsecure.pushsecure.PushSecureClient client; 80 | private DataProvider dataProvider; 81 | 82 | private PublishSubject newAccountObservable; 83 | 84 | @Override 85 | protected void onCreate(Bundle savedInstanceState) { 86 | super.onCreate(savedInstanceState); 87 | setContentView(R.layout.activity_main); 88 | ButterKnife.bind(this); 89 | 90 | setSupportActionBar(toolbar); 91 | final ActionBar actionBar = getSupportActionBar(); 92 | 93 | navigation.setNavigationItemSelectedListener(this); 94 | 95 | signOut.setOnClickListener(this); 96 | 97 | if (checkPlayServices()) { 98 | 99 | dataProvider = new DataProvider(this); 100 | 101 | client = new PushSecureClient("https://chatsecure-push.herokuapp.com/api/v1/"); 102 | //client = new PushSecureClient("http://10.11.41.186:8000/api/v1/"); 103 | 104 | register(); 105 | } 106 | processIntent(getIntent()); 107 | } 108 | 109 | @Override 110 | public boolean onOptionsItemSelected(MenuItem item) { 111 | switch (item.getItemId()) { 112 | case android.R.id.home: 113 | drawer.openDrawer(GravityCompat.START); 114 | return true; 115 | 116 | } 117 | 118 | return super.onOptionsItemSelected(item); 119 | } 120 | 121 | @Override 122 | protected void onNewIntent(Intent intent) { 123 | super.onNewIntent(intent); 124 | processIntent(intent); 125 | } 126 | 127 | private void processIntent(Intent intent) { 128 | if (intent.getAction().equals(GcmService.REVOKE_TOKEN_ACTION)) { 129 | handleRevokeTokenIntent(intent); 130 | } 131 | } 132 | 133 | private void setContentFragment(Fragment fragment, int titleResId) { 134 | getSupportFragmentManager().beginTransaction() 135 | .replace(R.id.container, fragment, null) 136 | .setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN) 137 | .commit(); 138 | 139 | ActionBar actionBar; 140 | if ((actionBar = getSupportActionBar()) != null) { 141 | actionBar.setTitle(titleResId); 142 | actionBar.setHomeAsUpIndicator(R.drawable.ic_menu_black_24dp); 143 | actionBar.setDisplayHomeAsUpEnabled(true); 144 | } 145 | } 146 | 147 | private void handleRevokeTokenIntent(Intent intent) { 148 | client.deleteToken(intent.getStringExtra(GcmService.TOKEN_EXTRA), new PushSecureClient.RequestCallback() { 149 | @Override 150 | public void onSuccess(Void response) { 151 | Timber.d("Delete token!"); 152 | handleRevokeTokenIntentProcessed(intent); 153 | } 154 | 155 | @Override 156 | public void onFailure(Throwable t) { 157 | Timber.e(t, "Failed to delete token"); 158 | handleRevokeTokenIntentProcessed(intent); 159 | } 160 | }); 161 | } 162 | 163 | private void handleRevokeTokenIntentProcessed(Intent intent) { 164 | dismissNotification(intent.getIntExtra(GcmService.NOTIFICATION_ID_EXTRA, 1)); 165 | Snackbar.make(container, getString(R.string.revoked_token), Snackbar.LENGTH_SHORT).show(); 166 | } 167 | 168 | private void dismissNotification(int id) { 169 | NotificationManager notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); 170 | notificationManager.cancel(id); 171 | } 172 | 173 | private void register() { 174 | Registration.register(RegistrationIntentService.refreshGcmToken(this), 175 | client, 176 | dataProvider, () -> { 177 | // Registration needed 178 | drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED); 179 | setContentFragment(RegistrationFragment.newInstance(client), R.string.registration); 180 | hideActionBar(); 181 | 182 | newAccountObservable = PublishSubject.create(); 183 | return newAccountObservable; 184 | }) 185 | .observeOn(AndroidSchedulers.mainThread()) 186 | .subscribe(pushSecureClient -> { 187 | Timber.d("Registered"); 188 | name.setText(dataProvider.getPushSecureUsername()); 189 | drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED); 190 | // Show a "Create / Share Whitelist token" Fragment 191 | setContentFragment(MessagingFragment.newInstance(pushSecureClient, dataProvider), R.string.messaging); 192 | }, throwable -> { 193 | Timber.e(throwable, "Failed to register device!"); 194 | }); 195 | } 196 | 197 | /** 198 | * Check the device to make sure it has the Google Play Services APK. If 199 | * it doesn't, display a dialog that allows users to download the APK from 200 | * the Google Play Store or enable it in the device's system settings. 201 | */ 202 | private boolean checkPlayServices() { 203 | int resultCode = GooglePlayServicesUtil.isGooglePlayServicesAvailable(this); 204 | if (resultCode != ConnectionResult.SUCCESS) { 205 | if (GooglePlayServicesUtil.isUserRecoverableError(resultCode)) { 206 | GooglePlayServicesUtil.getErrorDialog(resultCode, this, 207 | PLAY_SERVICES_RESOLUTION_REQUEST).show(); 208 | } else { 209 | Timber.i("This device is not supported."); 210 | finish(); 211 | } 212 | return false; 213 | } 214 | return true; 215 | } 216 | 217 | @Override 218 | public void onAccountCreated(Account account) { 219 | // PushSecureRegistration doesn't need the ChatSecure Push username 220 | // so our app is responsible for persisting it in order to personalize our UI 221 | dataProvider.setPushSecureUsername(account.username); 222 | 223 | // Notify PushSecureRegistration 224 | if (newAccountObservable != null) { 225 | newAccountObservable.onNext(account); 226 | 227 | // Change this when we support the user changing their account in-app 228 | newAccountObservable.onCompleted(); 229 | } 230 | 231 | } 232 | 233 | /** Navigation drawer item selection */ 234 | @Override 235 | public boolean onNavigationItemSelected(MenuItem menuItem) { 236 | 237 | switch (menuItem.getItemId()) { 238 | case R.id.my_tokens: 239 | setContentFragment(TokensFragment.newInstance(client, dataProvider), R.string.my_tokens); 240 | break; 241 | 242 | case R.id.my_devices: 243 | setContentFragment(DevicesFragment.newInstance(client, dataProvider), R.string.my_devices); 244 | break; 245 | 246 | case R.id.messaging: 247 | setContentFragment(MessagingFragment.newInstance(client, dataProvider), R.string.messaging); 248 | break; 249 | } 250 | 251 | menuItem.setChecked(true); 252 | drawer.closeDrawers(); 253 | return true; 254 | } 255 | 256 | @Override 257 | public void onClick(View v) { 258 | if (v.equals(signOut)) { 259 | dataProvider.clear(); 260 | client.setAccount(null); 261 | register(); 262 | drawer.closeDrawers(); 263 | } 264 | } 265 | 266 | private void hideActionBar() { 267 | ActionBar actionBar; 268 | if ((actionBar = getSupportActionBar()) != null) { 269 | actionBar.setDisplayHomeAsUpEnabled(false); 270 | actionBar.setTitle(""); 271 | } 272 | } 273 | } -------------------------------------------------------------------------------- /example/src/main/java/org/chatsecure/pushdemo/Registration.java: -------------------------------------------------------------------------------- 1 | package org.chatsecure.pushdemo; 2 | 3 | import android.support.annotation.NonNull; 4 | 5 | import org.chatsecure.pushsecure.PushSecureClient; 6 | import org.chatsecure.pushsecure.response.Account; 7 | import org.chatsecure.pushsecure.response.Device; 8 | 9 | import java.io.IOException; 10 | 11 | import rx.Observable; 12 | import rx.schedulers.Schedulers; 13 | import timber.log.Timber; 14 | 15 | public class Registration { 16 | 17 | /** 18 | * Wraps the process of ensuring this device is ready to request whitelist tokens 19 | * and send messages via the ChatSecure-Push API 20 | *

21 | * Flow: 22 | *

    23 | *
  1. Get the current GCM token and get a ChatSecure-Push auth token in parallel. If no CSP auth token is available 24 | * via {@param provider}, one is requested from this class's client via {@param callback}
  2. 25 | *
  3. Register the GCM token with the ChatSecure-Push account obtained in prior step
  4. 26 | *
27 | *

28 | * Created by davidbrodsky on 7/7/15. 29 | */ 30 | public static Observable register(@NonNull Observable getGcmToken, 31 | @NonNull PushSecureClient client, 32 | @NonNull DataProvider dataProvider, 33 | @NonNull PushServiceRegistrationCredentialsProvider callback) { 34 | 35 | return Observable.zip(getGcmToken, 36 | getOrCreateChatSecurePushAccount(dataProvider, callback).doOnNext(client::setAccount), 37 | (gcmToken, authToken) -> gcmToken) 38 | .observeOn(Schedulers.io()) 39 | .doOnNext(gcmToken -> registerDevice(gcmToken, dataProvider, client)) 40 | .map(gcmToken -> client); 41 | } 42 | 43 | /** 44 | * @return an observable for a ChatSecure-Push Authentication token 45 | * Either provided by storage or the client-provided callback 46 | */ 47 | private static Observable getOrCreateChatSecurePushAccount(DataProvider dataProvider, 48 | PushServiceRegistrationCredentialsProvider callback) { 49 | if (dataProvider.getPushSecureAuthToken() != null && dataProvider.getPushSecureUsername() != null) 50 | return Observable.just( 51 | new Account(dataProvider.getPushSecureUsername(), dataProvider.getPushSecureAuthToken(), null)); 52 | 53 | return callback.getChatSecurePushAccount() 54 | .doOnNext(account -> { 55 | dataProvider.setPushSecureUsername(account.username); 56 | dataProvider.setPushSecureAuthToken(account.token); 57 | }); 58 | } 59 | 60 | /** 61 | * Registers the given GCM Token with the ChatSecure-Push server if necessary, 62 | * and saves it to the dataprovider if it was successfully sent 63 | */ 64 | private static void registerDevice(String gcmToken, DataProvider dataProvider, PushSecureClient client) { 65 | if (dataProvider.getDevice() == null || !dataProvider.getDevice().registrationId.equals(gcmToken)) { 66 | Timber.d("Registering GCM token with ChatSecure-Push"); 67 | client.createDevice(gcmToken, "whateverDevice", null, new PushSecureClient.RequestCallback() { 68 | @Override 69 | public void onSuccess(Device response) { 70 | dataProvider.setDevice(response); 71 | } 72 | 73 | @Override 74 | public void onFailure(Throwable t) { 75 | Timber.e(t, "Failed to register device!"); 76 | } 77 | }); 78 | } else { 79 | Timber.d("GCM token already registered with ChatSecure-Push"); 80 | } 81 | // Do nothing. GCM Token already registered 82 | } 83 | 84 | public interface PushServiceRegistrationCredentialsProvider { 85 | /** 86 | * @return an observable for a newly created ChatSecure Push Account. Persisting the account is handled internally. 87 | */ 88 | Observable getChatSecurePushAccount(); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /example/src/main/java/org/chatsecure/pushdemo/gcm/GcmService.java: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2015 Google Inc. All Rights Reserved. 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 org.chatsecure.pushdemo.gcm; 17 | 18 | import android.app.NotificationManager; 19 | import android.app.PendingIntent; 20 | import android.content.Intent; 21 | import android.os.Bundle; 22 | import android.support.v7.app.NotificationCompat; 23 | 24 | import com.google.android.gms.gcm.GcmListenerService; 25 | 26 | import org.chatsecure.pushdemo.MainActivity; 27 | import org.chatsecure.pushdemo.R; 28 | import org.chatsecure.pushsecure.gcm.PushMessage; 29 | import org.chatsecure.pushsecure.gcm.PushParser; 30 | 31 | import java.util.Random; 32 | 33 | 34 | /** 35 | * Service used for receiving GCM messages. When a message is received this service will log it. 36 | */ 37 | public class GcmService extends GcmListenerService { 38 | 39 | /** 40 | * Intent Action 41 | */ 42 | public static final String REVOKE_TOKEN_ACTION = "org.chatsecure.blocktoken"; 43 | 44 | /** 45 | * Token Intents 46 | */ 47 | public static final String TOKEN_EXTRA = "token"; 48 | public static final String NOTIFICATION_ID_EXTRA = "notId"; 49 | 50 | /** 51 | * PendingIntent Request Codes 52 | */ 53 | public static final int BLOCK_SENDER_REQUEST = 100; 54 | 55 | private PushParser parser = new PushParser(); 56 | 57 | public GcmService() { 58 | } 59 | 60 | @Override 61 | public void onMessageReceived(String from, Bundle data) { 62 | 63 | PushMessage pushSecureMessage = parser.parseBundle(from, data); 64 | 65 | if (pushSecureMessage != null) 66 | postNotification(pushSecureMessage.payload, pushSecureMessage.token); 67 | } 68 | 69 | @Override 70 | public void onDeletedMessages() { 71 | postNotification("Deleted messages on server"); 72 | } 73 | 74 | @Override 75 | public void onMessageSent(String msgId) { 76 | postNotification("Upstream message sent. Id=" + msgId); 77 | } 78 | 79 | @Override 80 | public void onSendError(String msgId, String error) { 81 | postNotification("Upstream message send error. Id=" + msgId + ", error" + error); 82 | } 83 | 84 | private void postNotification(String msg) { 85 | postNotification(msg, null); 86 | } 87 | 88 | private void postNotification(String msg, String fromToken) { 89 | int notificationId = new Random().nextInt(Integer.MAX_VALUE); 90 | NotificationCompat.Builder builder = new NotificationCompat.Builder(getApplicationContext()); 91 | builder.setContentTitle(msg); 92 | if (fromToken != null) builder.setContentText("From " + fromToken); 93 | builder.setVibrate(new long[]{250, 250}); 94 | builder.setSmallIcon(R.mipmap.ic_launcher); 95 | builder.setPriority(NotificationCompat.PRIORITY_MAX); 96 | 97 | Intent blockIntent = new Intent(this, MainActivity.class); 98 | blockIntent.setAction(REVOKE_TOKEN_ACTION); 99 | blockIntent.putExtra(TOKEN_EXTRA, fromToken); 100 | blockIntent.putExtra(NOTIFICATION_ID_EXTRA, notificationId); 101 | blockIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 102 | PendingIntent blockPendingIntent = 103 | PendingIntent.getActivity( 104 | this, 105 | BLOCK_SENDER_REQUEST, 106 | blockIntent, 107 | PendingIntent.FLAG_UPDATE_CURRENT); 108 | 109 | builder.addAction(R.drawable.ic_block, "Block Sender", blockPendingIntent); 110 | 111 | NotificationManager notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); 112 | notificationManager.notify(notificationId, builder.build()); 113 | } 114 | } -------------------------------------------------------------------------------- /example/src/main/java/org/chatsecure/pushdemo/gcm/MyInstanceIDListenerService.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2015 Google Inc. All Rights Reserved. 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 | 17 | package org.chatsecure.pushdemo.gcm; 18 | 19 | import android.content.Intent; 20 | 21 | import com.google.android.gms.iid.InstanceIDListenerService; 22 | 23 | public class MyInstanceIDListenerService extends InstanceIDListenerService { 24 | 25 | /** 26 | * Called if InstanceID token is updated. This may occur if the security of 27 | * the previous token had been compromised. This call is initiated by the 28 | * InstanceID provider. 29 | */ 30 | @Override 31 | public void onTokenRefresh() { 32 | // Fetch updated Instance ID token and notify our app's server of any changes (if applicable). 33 | Intent intent = new Intent(this, RegistrationIntentService.class); 34 | startService(intent); 35 | } 36 | } -------------------------------------------------------------------------------- /example/src/main/java/org/chatsecure/pushdemo/gcm/RegistrationIntentService.java: -------------------------------------------------------------------------------- 1 | package org.chatsecure.pushdemo.gcm; 2 | 3 | import android.app.IntentService; 4 | import android.content.Context; 5 | import android.content.Intent; 6 | import android.support.annotation.NonNull; 7 | import android.util.Log; 8 | 9 | import com.google.android.gms.gcm.GoogleCloudMessaging; 10 | import com.google.android.gms.iid.InstanceID; 11 | 12 | import org.chatsecure.pushdemo.R; 13 | 14 | import rx.Observable; 15 | import rx.subjects.PublishSubject; 16 | import timber.log.Timber; 17 | 18 | public class RegistrationIntentService extends IntentService { 19 | 20 | private static final String TAG = "RegIntentService"; 21 | private static final String[] TOPICS = {"global"}; 22 | 23 | private static PublishSubject gcmTokenSubject = PublishSubject.create(); 24 | private static Observable gcmTokenObservable; 25 | 26 | /** 27 | * Convenience method to retrieve refreshed GCM token as an {@link Observable}. 28 | * This will only perform a network request to GCM at most once per process lifecycle 29 | */ 30 | public static Observable refreshGcmToken(@NonNull Context packageContext) { 31 | // We expect to receive only one token per process lifecycle so cache the first result 32 | // for all clients 33 | if (gcmTokenObservable == null) { 34 | gcmTokenObservable = gcmTokenSubject.cache(1); 35 | 36 | Intent intent = new Intent(packageContext, RegistrationIntentService.class); 37 | packageContext.startService(intent); 38 | } 39 | return gcmTokenObservable; 40 | } 41 | 42 | public RegistrationIntentService() { 43 | super(TAG); 44 | } 45 | 46 | @Override 47 | protected void onHandleIntent(Intent intent) { 48 | try { 49 | // In the (unlikely) event that multiple refresh operations occur simultaneously, 50 | // ensure that they are processed sequentially. 51 | synchronized (TAG) { 52 | // [START register_for_gcm] 53 | // Initially this call goes out to the network to retrieve the token, subsequent calls 54 | // are local. 55 | // [START get_token] 56 | InstanceID instanceID = InstanceID.getInstance(this); 57 | String token = instanceID.getToken(getString(R.string.gcm_defaultSenderId), 58 | GoogleCloudMessaging.INSTANCE_ID_SCOPE, null); 59 | // [END get_token] 60 | Log.i(TAG, "GCM Registration Token: " + token); 61 | 62 | // TODO: Implement this method to send any registration to your app's servers. 63 | if (gcmTokenSubject != null) gcmTokenSubject.onNext(token); 64 | 65 | // Subscribe to topic channels 66 | //subscribeTopics(token); 67 | 68 | // You should store a boolean that indicates whether the generated token has been 69 | // sent to your server. If the boolean is false, send the token to your server, 70 | // otherwise your server should have already received the token. 71 | //sharedPreferences.edit().putBoolean(QuickstartPreferences.SENT_TOKEN_TO_SERVER, true).apply(); 72 | // [END register_for_gcm] 73 | } 74 | } catch (Exception e) { 75 | String errorMessage = "Failed to obtain GCM token"; 76 | Timber.e(e, errorMessage); 77 | // If an exception happens while fetching the new token or updating our registration data 78 | // on a third-party server, this ensures that we'll attempt the update at a later time. 79 | gcmTokenSubject.onError(new IllegalStateException(errorMessage, e)); 80 | } 81 | } 82 | } -------------------------------------------------------------------------------- /example/src/main/java/org/chatsecure/pushdemo/ui/adapter/DeviceAdapter.java: -------------------------------------------------------------------------------- 1 | package org.chatsecure.pushdemo.ui.adapter; 2 | 3 | import android.graphics.Typeface; 4 | import android.support.v7.widget.RecyclerView; 5 | import android.view.LayoutInflater; 6 | import android.view.View; 7 | import android.view.ViewGroup; 8 | import android.widget.Button; 9 | import android.widget.TextView; 10 | 11 | import org.chatsecure.pushdemo.R; 12 | import org.chatsecure.pushsecure.response.Device; 13 | 14 | import java.text.SimpleDateFormat; 15 | import java.util.ArrayList; 16 | import java.util.List; 17 | import java.util.Locale; 18 | 19 | /** 20 | * Binds {@link org.chatsecure.pushsecure.response.PushToken}s to views within a {@link RecyclerView} 21 | * Created by dbro on 7/27/15. 22 | */ 23 | public class DeviceAdapter extends RecyclerView.Adapter { 24 | 25 | public List devices = new ArrayList<>(); 26 | 27 | private SimpleDateFormat sdf = new SimpleDateFormat("EE M/d/yyyy h:mm a", Locale.US); 28 | private Listener listener; 29 | private Device thisDevice; 30 | 31 | public DeviceAdapter(Device thisDevice, Listener listener) { 32 | this(listener); 33 | this.thisDevice = thisDevice; 34 | } 35 | 36 | public DeviceAdapter(Listener listener) { 37 | this.listener = listener; 38 | } 39 | 40 | public void setDevices(List devices) { 41 | this.devices = devices; 42 | notifyDataSetChanged(); 43 | } 44 | 45 | public void removeDevice(Device device) { 46 | int idx = devices.indexOf(device); 47 | devices.remove(idx); 48 | notifyItemRemoved(idx); 49 | } 50 | 51 | @Override 52 | public ViewHolder onCreateViewHolder(ViewGroup viewGroup, int viewType) { 53 | View v = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.device_item, viewGroup, false); 54 | return new ViewHolder(v); 55 | } 56 | 57 | @Override 58 | public void onBindViewHolder(ViewHolder viewHolder, int position) { 59 | Device device = devices.get(position); 60 | 61 | String name = device.name != null ? device.name : "Untitled device"; 62 | viewHolder.name.setText(name + String.format(" (%s)", device.type)); 63 | viewHolder.createdDate.setText("Created " + sdf.format(device.dateCreated)); 64 | viewHolder.revoke.setTag(device); 65 | viewHolder.revoke.setText(R.string.revoke); 66 | 67 | if (thisDevice != null && thisDevice.id.equals(device.id)) { 68 | viewHolder.name.setTypeface(viewHolder.name.getTypeface(), Typeface.BOLD); 69 | } else { 70 | viewHolder.name.setTypeface(viewHolder.name.getTypeface(), Typeface.NORMAL); 71 | } 72 | 73 | } 74 | 75 | @Override 76 | public int getItemCount() { 77 | return devices.size(); 78 | } 79 | 80 | class ViewHolder extends RecyclerView.ViewHolder { 81 | 82 | TextView name; 83 | TextView createdDate; 84 | 85 | Button revoke; 86 | 87 | public ViewHolder(View itemView) { 88 | super(itemView); 89 | 90 | name = (TextView) itemView.findViewById(R.id.name); 91 | createdDate = (TextView) itemView.findViewById(R.id.createdDate); 92 | revoke = (Button) itemView.findViewById(R.id.revokeButton); 93 | 94 | revoke.setOnClickListener(v -> { 95 | v.setEnabled(false); 96 | ((Button) v).setText(R.string.revoking); 97 | listener.onRevokeDeviceRequested((Device) v.getTag()); 98 | }); 99 | } 100 | } 101 | 102 | public interface Listener { 103 | 104 | /** 105 | * Handle a user request to revoke the given device. When the device is successfully 106 | * revoked, call {@link #removeDevice(Device)}} to notify the adapter 107 | * @param device the device to revoke 108 | */ 109 | void onRevokeDeviceRequested(Device device); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /example/src/main/java/org/chatsecure/pushdemo/ui/adapter/TokenAdapter.java: -------------------------------------------------------------------------------- 1 | package org.chatsecure.pushdemo.ui.adapter; 2 | 3 | import android.support.v7.widget.RecyclerView; 4 | import android.view.LayoutInflater; 5 | import android.view.View; 6 | import android.view.ViewGroup; 7 | import android.widget.Button; 8 | import android.widget.TextView; 9 | 10 | import org.chatsecure.pushdemo.R; 11 | import org.chatsecure.pushsecure.response.PushToken; 12 | 13 | import java.util.ArrayList; 14 | import java.util.List; 15 | 16 | /** 17 | * Binds {@link org.chatsecure.pushsecure.response.PushToken}s to views within a {@link RecyclerView} 18 | * Created by dbro on 7/27/15. 19 | */ 20 | public class TokenAdapter extends RecyclerView.Adapter { 21 | 22 | public List tokens = new ArrayList<>(); 23 | 24 | private Listener listener; 25 | 26 | public TokenAdapter(Listener listener) { 27 | this.listener = listener; 28 | } 29 | 30 | public void removeToken(PushToken token) { 31 | int idx = tokens.indexOf(token); 32 | tokens.remove(idx); 33 | notifyItemRemoved(idx); 34 | } 35 | 36 | public void setTokens(List tokens) { 37 | this.tokens = tokens; 38 | notifyDataSetChanged(); 39 | } 40 | 41 | @Override 42 | public ViewHolder onCreateViewHolder(ViewGroup viewGroup, int viewType) { 43 | View v = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.token_item, viewGroup, false); 44 | return new ViewHolder(v); 45 | } 46 | 47 | @Override 48 | public void onBindViewHolder(ViewHolder viewHolder, int position) { 49 | PushToken token = tokens.get(position); 50 | 51 | viewHolder.name.setText(token.name != null ? token.name : "Untitled token"); 52 | viewHolder.token.setText("Token: " + token.token); 53 | viewHolder.device.setText("Device: " + token.getDeviceIdentifier()); 54 | 55 | viewHolder.revoke.setTag(token); 56 | viewHolder.revoke.setText(R.string.revoke); 57 | 58 | } 59 | 60 | @Override 61 | public int getItemCount() { 62 | return tokens.size(); 63 | } 64 | 65 | class ViewHolder extends RecyclerView.ViewHolder { 66 | 67 | TextView name; 68 | TextView token; 69 | TextView device; 70 | 71 | Button revoke; 72 | 73 | public ViewHolder(View itemView) { 74 | super(itemView); 75 | 76 | revoke = (Button) itemView.findViewById(R.id.revokeButton); 77 | name = (TextView) itemView.findViewById(R.id.name); 78 | token = (TextView) itemView.findViewById(R.id.token); 79 | device = (TextView) itemView.findViewById(R.id.device); 80 | 81 | revoke.setOnClickListener(v -> { 82 | v.setEnabled(false); 83 | ((Button) v).setText("Revoking..."); 84 | listener.onRevokeTokenRequested((PushToken) v.getTag()); 85 | }); 86 | } 87 | } 88 | 89 | public interface Listener { 90 | 91 | /** 92 | * Handle a user request to revoke the given token. When the token is successfully 93 | * revoked, call {@link #removeToken(PushToken)} to notify the adapter 94 | * @param token the token to revoke 95 | */ 96 | void onRevokeTokenRequested(PushToken token); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /example/src/main/java/org/chatsecure/pushdemo/ui/fragment/DevicesFragment.java: -------------------------------------------------------------------------------- 1 | package org.chatsecure.pushdemo.ui.fragment; 2 | 3 | import android.os.Bundle; 4 | import android.support.design.widget.Snackbar; 5 | import android.support.v4.app.Fragment; 6 | import android.support.v7.widget.LinearLayoutManager; 7 | import android.support.v7.widget.RecyclerView; 8 | import android.view.LayoutInflater; 9 | import android.view.View; 10 | import android.view.ViewGroup; 11 | import android.widget.ProgressBar; 12 | import android.widget.TextView; 13 | 14 | import org.chatsecure.pushdemo.DataProvider; 15 | import org.chatsecure.pushdemo.R; 16 | import org.chatsecure.pushdemo.ui.adapter.DeviceAdapter; 17 | import org.chatsecure.pushsecure.PushSecureClient; 18 | import org.chatsecure.pushsecure.response.Device; 19 | import org.chatsecure.pushsecure.response.DeviceList; 20 | 21 | import java.io.IOException; 22 | 23 | import butterknife.Bind; 24 | import butterknife.ButterKnife; 25 | import rx.Observable; 26 | import rx.android.schedulers.AndroidSchedulers; 27 | import timber.log.Timber; 28 | 29 | /** 30 | * UI for managing a user's devices 31 | */ 32 | public class DevicesFragment extends Fragment implements DeviceAdapter.Listener { 33 | 34 | private PushSecureClient client; 35 | private DataProvider provider; 36 | private DeviceAdapter adapter; 37 | 38 | @Bind(R.id.recyclerView) 39 | RecyclerView recyclerView; 40 | 41 | @Bind(R.id.progressBar) 42 | ProgressBar progressIndicator; 43 | 44 | @Bind(R.id.emptyText) 45 | TextView emptyText; 46 | 47 | public static DevicesFragment newInstance(PushSecureClient client, DataProvider provider) { 48 | DevicesFragment fragment = new DevicesFragment(); 49 | fragment.setPushSecureClient(client); 50 | fragment.setDataProvider(provider); 51 | return fragment; 52 | } 53 | 54 | public DevicesFragment() { 55 | // Required empty public constructor 56 | } 57 | 58 | public void setPushSecureClient(PushSecureClient client) { 59 | this.client = client; 60 | } 61 | 62 | public void setDataProvider(DataProvider provider) { 63 | this.provider = provider; 64 | } 65 | 66 | @Override 67 | public void onCreate(Bundle savedInstanceState) { 68 | super.onCreate(savedInstanceState); 69 | } 70 | 71 | @Override 72 | public View onCreateView(LayoutInflater inflater, ViewGroup container, 73 | Bundle savedInstanceState) { 74 | // Inflate the layout for this fragment 75 | View root = inflater.inflate(R.layout.recyclerview, container, false); 76 | ButterKnife.bind(this, root); 77 | 78 | adapter = new DeviceAdapter(provider.getDevice(), this); 79 | recyclerView.setLayoutManager(new LinearLayoutManager(getActivity())); 80 | recyclerView.setAdapter(adapter); 81 | displayDevices(); 82 | return root; 83 | } 84 | 85 | private void displayDevices() { 86 | // TODO : Combined APNS + GCM Devices call 87 | client.getGcmDevices(new PushSecureClient.RequestCallback() { 88 | @Override 89 | public void onSuccess(DeviceList response) { 90 | adapter.setDevices(response.results); 91 | progressIndicator.setVisibility(View.GONE); 92 | maybeDisplayEmptyText(); 93 | } 94 | 95 | @Override 96 | public void onFailure(Throwable t) { 97 | String message = "Failed to fetch devices"; 98 | Timber.e(t, message); 99 | Snackbar.make(recyclerView, message, Snackbar.LENGTH_SHORT) 100 | .show(); 101 | } 102 | }); 103 | } 104 | 105 | private void maybeDisplayEmptyText() { 106 | if (adapter.getItemCount() == 0) { 107 | emptyText.setText(R.string.you_have_no_devices); 108 | emptyText.setVisibility(View.VISIBLE); 109 | } else { 110 | emptyText.setVisibility(View.GONE); 111 | } 112 | } 113 | 114 | @Override 115 | public void onRevokeDeviceRequested(Device device) { 116 | client.deleteDevice(device.id, new PushSecureClient.RequestCallback() { 117 | @Override 118 | public void onSuccess(Void response) { 119 | Timber.d("Delete token"); 120 | adapter.removeDevice(device); 121 | maybeDisplayEmptyText(); 122 | } 123 | 124 | @Override 125 | public void onFailure(Throwable throwable) { 126 | String message = "Failed to delete token"; 127 | Timber.e(throwable, message); 128 | Snackbar.make(recyclerView, message, Snackbar.LENGTH_SHORT) 129 | .show(); 130 | } 131 | }); 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /example/src/main/java/org/chatsecure/pushdemo/ui/fragment/MessagingFragment.java: -------------------------------------------------------------------------------- 1 | package org.chatsecure.pushdemo.ui.fragment; 2 | 3 | import android.content.Intent; 4 | import android.os.Bundle; 5 | import android.support.annotation.NonNull; 6 | import android.support.design.widget.Snackbar; 7 | import android.support.v4.app.Fragment; 8 | import android.view.LayoutInflater; 9 | import android.view.View; 10 | import android.view.ViewGroup; 11 | import android.widget.Button; 12 | import android.widget.EditText; 13 | 14 | import org.chatsecure.pushdemo.DataProvider; 15 | import org.chatsecure.pushdemo.R; 16 | import org.chatsecure.pushsecure.PushSecureClient; 17 | import org.chatsecure.pushsecure.response.Message; 18 | import org.chatsecure.pushsecure.response.PushToken; 19 | 20 | import butterknife.Bind; 21 | import butterknife.ButterKnife; 22 | import retrofit.Callback; 23 | import retrofit.Response; 24 | import timber.log.Timber; 25 | 26 | /** 27 | * UI for obtaining and sharing push tokens on behalf of the current account, 28 | * as well as sending data payloads to push tokens. 29 | */ 30 | public class MessagingFragment extends Fragment implements View.OnClickListener { 31 | 32 | private PushSecureClient client; 33 | private DataProvider provider; 34 | 35 | @Bind(R.id.container) 36 | ViewGroup container; 37 | 38 | @Bind(R.id.sharePushTokenButton) 39 | Button shareTokenButton; 40 | 41 | @Bind(R.id.payloadEditText) 42 | EditText payloadEditText; 43 | 44 | @Bind(R.id.peerTokenEditText) 45 | EditText peerTokenEditText; 46 | 47 | @Bind(R.id.sendMessageButton) 48 | Button sendMessageButton; 49 | 50 | public static MessagingFragment newInstance(PushSecureClient client, DataProvider provider) { 51 | MessagingFragment fragment = new MessagingFragment(); 52 | fragment.setPushSecureClient(client); 53 | fragment.setDataProvider(provider); 54 | return fragment; 55 | } 56 | 57 | public MessagingFragment() { 58 | // Required empty public constructor 59 | } 60 | 61 | public void setPushSecureClient(PushSecureClient client) { 62 | this.client = client; 63 | } 64 | 65 | public void setDataProvider(DataProvider provider) { 66 | this.provider = provider; 67 | } 68 | 69 | @Override 70 | public void onCreate(Bundle savedInstanceState) { 71 | super.onCreate(savedInstanceState); 72 | } 73 | 74 | @Override 75 | public View onCreateView(LayoutInflater inflater, ViewGroup container, 76 | Bundle savedInstanceState) { 77 | // Inflate the layout for this fragment 78 | View root = inflater.inflate(R.layout.fragment_messaging, container, false); 79 | ButterKnife.bind(this, root); 80 | 81 | shareTokenButton.setOnClickListener(this); 82 | sendMessageButton.setOnClickListener(this); 83 | 84 | return root; 85 | } 86 | 87 | @Override 88 | public void onClick(@NonNull final View button) { 89 | switch (button.getId()) { 90 | case R.id.sharePushTokenButton: 91 | 92 | button.setEnabled(false); 93 | client.createToken(provider.getDevice(), null, new PushSecureClient.RequestCallback() { 94 | @Override 95 | public void onSuccess(PushToken response) { 96 | button.setEnabled(true); 97 | Intent shareIntent = new Intent(); 98 | shareIntent.setAction(Intent.ACTION_SEND); 99 | shareIntent.setType("text/plain"); 100 | shareIntent.putExtra(Intent.EXTRA_TEXT, response.token); 101 | startActivity(Intent.createChooser(shareIntent, "Share Push Token")); 102 | } 103 | 104 | @Override 105 | public void onFailure(Throwable t) { 106 | Timber.e(t, "Error fetching new token"); 107 | button.setEnabled(true); 108 | Snackbar.make(container, "Error fetching new token", Snackbar.LENGTH_SHORT) 109 | .show(); 110 | } 111 | }); 112 | break; 113 | 114 | case R.id.sendMessageButton: 115 | 116 | button.setEnabled(false); 117 | client.sendMessage(peerTokenEditText.getText().toString(), payloadEditText.getText().toString(), new PushSecureClient.RequestCallback() { 118 | @Override 119 | public void onSuccess(Message response) { 120 | button.setEnabled(true); 121 | String feedbackMessage = "Sent Message"; 122 | Timber.d(feedbackMessage); 123 | Snackbar.make(container, feedbackMessage, Snackbar.LENGTH_SHORT) 124 | .show(); 125 | } 126 | 127 | @Override 128 | public void onFailure(Throwable throwable) { 129 | String message = "Error sending message."; 130 | if (throwable instanceof PushSecureClient.RequestException && ((PushSecureClient.RequestException) throwable).getResponse().code() == 404) 131 | message += " Push token may be invalid."; 132 | 133 | button.setEnabled(true); 134 | Timber.e(throwable, message); 135 | Snackbar.make(container, message, Snackbar.LENGTH_SHORT) 136 | .show(); 137 | } 138 | }); 139 | break; 140 | } 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /example/src/main/java/org/chatsecure/pushdemo/ui/fragment/RegistrationFragment.java: -------------------------------------------------------------------------------- 1 | package org.chatsecure.pushdemo.ui.fragment; 2 | 3 | import android.app.Activity; 4 | import android.os.Bundle; 5 | import android.support.design.widget.Snackbar; 6 | import android.support.design.widget.TextInputLayout; 7 | import android.support.v4.app.Fragment; 8 | import android.text.TextUtils; 9 | import android.view.LayoutInflater; 10 | import android.view.View; 11 | import android.view.ViewGroup; 12 | import android.widget.Button; 13 | import android.widget.EditText; 14 | 15 | import com.jakewharton.rxbinding.widget.RxTextView; 16 | 17 | import org.chatsecure.pushdemo.R; 18 | import org.chatsecure.pushsecure.PushSecureClient; 19 | import org.chatsecure.pushsecure.response.Account; 20 | 21 | import butterknife.Bind; 22 | import butterknife.ButterKnife; 23 | import retrofit.Callback; 24 | import retrofit.Response; 25 | import rx.Observable; 26 | import rx.Subscription; 27 | import timber.log.Timber; 28 | 29 | /** 30 | * Account registration UI 31 | *

32 | * The host {@link Activity} must implement {@link AccountRegistrationListener} to be notified 33 | * of account creation. 34 | */ 35 | public class RegistrationFragment extends Fragment implements View.OnClickListener { 36 | 37 | @Bind(R.id.usernameLayout) 38 | TextInputLayout usernameLayout; 39 | 40 | @Bind(R.id.username) 41 | EditText usernameEditText; 42 | 43 | @Bind(R.id.passwordLayout) 44 | TextInputLayout passwordLayout; 45 | 46 | @Bind(R.id.password) 47 | EditText passwordEditText; 48 | 49 | @Bind(R.id.createAccountButton) 50 | Button signupButton; 51 | 52 | @Bind(R.id.container) 53 | ViewGroup container; 54 | 55 | private PushSecureClient client; 56 | private AccountRegistrationListener mListener; 57 | private Subscription userInputSubscription; 58 | 59 | public static RegistrationFragment newInstance(PushSecureClient client) { 60 | RegistrationFragment frag = new RegistrationFragment(); 61 | frag.setPushSecureClient(client); 62 | return frag; 63 | } 64 | 65 | public RegistrationFragment() { 66 | // Required empty public constructor 67 | } 68 | 69 | public void setPushSecureClient(PushSecureClient client) { 70 | this.client = client; 71 | } 72 | 73 | @Override 74 | public View onCreateView(LayoutInflater inflater, ViewGroup container, 75 | Bundle savedInstanceState) { 76 | // Inflate the layout for this fragment 77 | View root = inflater.inflate(R.layout.fragment_push_secure_registration, container, false); 78 | ButterKnife.bind(this, root); 79 | 80 | userInputSubscription = 81 | Observable.merge(RxTextView.textChanges(usernameEditText), 82 | RxTextView.textChanges(passwordEditText)) 83 | 84 | .distinctUntilChanged(textChangedEvent -> 85 | usernameEditText.getText().hashCode() ^ 86 | passwordEditText.getText().hashCode()) 87 | 88 | .subscribe(onTextChangeEvent -> { 89 | signupButton.setVisibility(checkUserPasswordEntry(false) ? View.VISIBLE : View.INVISIBLE); 90 | }); 91 | 92 | passwordEditText.setOnEditorActionListener((view, actionId, event) -> { 93 | if (checkUserPasswordEntry(true)) { 94 | onClick(null); 95 | } 96 | return false; 97 | }); 98 | 99 | signupButton.setOnClickListener(this); 100 | return root; 101 | } 102 | 103 | @Override 104 | public void onDestroyView () { 105 | super.onDestroyView(); 106 | 107 | if (userInputSubscription != null) { 108 | userInputSubscription.unsubscribe(); 109 | userInputSubscription = null; 110 | } 111 | } 112 | 113 | @Override 114 | public void onAttach(Activity activity) { 115 | super.onAttach(activity); 116 | try { 117 | mListener = (AccountRegistrationListener) activity; 118 | } catch (ClassCastException e) { 119 | throw new ClassCastException(activity.toString() 120 | + " must implement OnFragmentInteractionListener"); 121 | } 122 | } 123 | 124 | @Override 125 | public void onDetach() { 126 | super.onDetach(); 127 | mListener = null; 128 | } 129 | 130 | @Override 131 | public void onClick(View v) { 132 | 133 | setEntryViewsEnabled(false); 134 | signupButton.setText(R.string.signing_up); 135 | 136 | String username = usernameEditText.getText().toString(); 137 | String password = passwordEditText.getText().toString(); 138 | 139 | if (client == null) throw new IllegalStateException("PushSecureClient not set!"); 140 | 141 | client.authenticateAccount(username, password, null, new PushSecureClient.RequestCallback() { 142 | @Override 143 | public void onSuccess(Account response) { 144 | mListener.onAccountCreated(response); 145 | } 146 | 147 | @Override 148 | public void onFailure(Throwable throwable) { 149 | Timber.e(throwable, getActivity().getString(R.string.failed_to_create_account)); 150 | setEntryViewsEnabled(true); 151 | signupButton.setText(R.string.create_account); 152 | Snackbar.make(container, R.string.failed_to_create_account, Snackbar.LENGTH_LONG) 153 | .show(); 154 | } 155 | }); 156 | } 157 | 158 | private boolean checkUserPasswordEntry(boolean showError) { 159 | boolean usernameValid = !TextUtils.isEmpty(usernameEditText.getText()); 160 | boolean passwordValid = !TextUtils.isEmpty(passwordEditText.getText()); 161 | 162 | if (showError) { 163 | usernameLayout.setError(usernameValid ? null : "Enter a username"); 164 | passwordLayout.setError(passwordValid ? null : "Enter a password"); 165 | } else { 166 | // Even if error show not requested, we should always clear errors 167 | // that are no longer valid 168 | if (usernameValid) usernameLayout.setError(null); 169 | if (passwordValid) passwordLayout.setError(null); 170 | } 171 | 172 | return usernameValid && passwordValid; 173 | } 174 | 175 | private void setEntryViewsEnabled(boolean isEnabled) { 176 | usernameEditText.setEnabled(isEnabled); 177 | passwordEditText.setEnabled(isEnabled); 178 | signupButton.setEnabled(isEnabled); 179 | } 180 | 181 | public interface AccountRegistrationListener { 182 | void onAccountCreated(Account account); 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /example/src/main/java/org/chatsecure/pushdemo/ui/fragment/TokensFragment.java: -------------------------------------------------------------------------------- 1 | package org.chatsecure.pushdemo.ui.fragment; 2 | 3 | import android.os.Bundle; 4 | import android.support.design.widget.Snackbar; 5 | import android.support.v4.app.Fragment; 6 | import android.support.v7.widget.LinearLayoutManager; 7 | import android.support.v7.widget.RecyclerView; 8 | import android.view.LayoutInflater; 9 | import android.view.View; 10 | import android.view.ViewGroup; 11 | import android.widget.ProgressBar; 12 | import android.widget.TextView; 13 | 14 | import org.chatsecure.pushdemo.DataProvider; 15 | import org.chatsecure.pushdemo.R; 16 | import org.chatsecure.pushdemo.ui.adapter.TokenAdapter; 17 | import org.chatsecure.pushsecure.PushSecureClient; 18 | import org.chatsecure.pushsecure.response.PushToken; 19 | import org.chatsecure.pushsecure.response.TokenList; 20 | 21 | import butterknife.Bind; 22 | import butterknife.ButterKnife; 23 | import retrofit.Callback; 24 | import retrofit.Response; 25 | import timber.log.Timber; 26 | 27 | /** 28 | * UI for managing a user's tokens 29 | */ 30 | public class TokensFragment extends Fragment implements TokenAdapter.Listener { 31 | 32 | private PushSecureClient client; 33 | private DataProvider provider; 34 | private TokenAdapter adapter; 35 | 36 | @Bind(R.id.recyclerView) 37 | RecyclerView recyclerView; 38 | 39 | @Bind(R.id.progressBar) 40 | ProgressBar progressIndicator; 41 | 42 | @Bind(R.id.emptyText) 43 | TextView emptyText; 44 | 45 | public static TokensFragment newInstance(PushSecureClient client, DataProvider provider) { 46 | TokensFragment fragment = new TokensFragment(); 47 | fragment.setPushSecureClient(client); 48 | fragment.setDataProvider(provider); 49 | return fragment; 50 | } 51 | 52 | public TokensFragment() { 53 | // Required empty public constructor 54 | } 55 | 56 | public void setPushSecureClient(PushSecureClient client) { 57 | this.client = client; 58 | } 59 | 60 | public void setDataProvider(DataProvider provider) { 61 | this.provider = provider; 62 | } 63 | 64 | @Override 65 | public void onCreate(Bundle savedInstanceState) { 66 | super.onCreate(savedInstanceState); 67 | } 68 | 69 | @Override 70 | public View onCreateView(LayoutInflater inflater, ViewGroup container, 71 | Bundle savedInstanceState) { 72 | View root = inflater.inflate(R.layout.recyclerview, container, false); 73 | ButterKnife.bind(this, root); 74 | adapter = new TokenAdapter(this); 75 | recyclerView.setLayoutManager(new LinearLayoutManager(getActivity())); 76 | recyclerView.setAdapter(adapter); 77 | displayTokens(); 78 | return root; 79 | } 80 | 81 | private void displayTokens() { 82 | client.getTokens(new PushSecureClient.RequestCallback() { 83 | @Override 84 | public void onSuccess(TokenList response) { 85 | adapter.setTokens(response.results); 86 | progressIndicator.setVisibility(View.GONE); 87 | maybeDisplayEmptyText(); 88 | } 89 | 90 | @Override 91 | public void onFailure(Throwable throwable) { 92 | String message = "Failed to fetch tokens"; 93 | Timber.e(throwable, message); 94 | Snackbar.make(recyclerView, message, Snackbar.LENGTH_SHORT) 95 | .show(); 96 | } 97 | }); 98 | } 99 | 100 | private void maybeDisplayEmptyText() { 101 | if (adapter.getItemCount() == 0) { 102 | emptyText.setText(R.string.you_have_no_tokens); 103 | emptyText.setVisibility(View.VISIBLE); 104 | } else { 105 | emptyText.setVisibility(View.GONE); 106 | } 107 | } 108 | 109 | @Override 110 | public void onRevokeTokenRequested(PushToken token) { 111 | client.deleteToken(token.token, new PushSecureClient.RequestCallback() { 112 | @Override 113 | public void onSuccess(Void response) { 114 | Timber.d("Delete token"); 115 | adapter.removeToken(token); 116 | maybeDisplayEmptyText(); 117 | } 118 | 119 | @Override 120 | public void onFailure(Throwable throwable) { 121 | String message = "Failed to delete token"; 122 | Timber.e(throwable, message); 123 | Snackbar.make(recyclerView, message, Snackbar.LENGTH_SHORT) 124 | .show(); 125 | } 126 | }); 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /example/src/main/res/drawable-hdpi-v11/ic_block.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChatSecure/ChatSecure-Push-Android/2cf8f066bcf2ae062a84d63bdb324a89a3b9534c/example/src/main/res/drawable-hdpi-v11/ic_block.png -------------------------------------------------------------------------------- /example/src/main/res/drawable-hdpi-v9/ic_block.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChatSecure/ChatSecure-Push-Android/2cf8f066bcf2ae062a84d63bdb324a89a3b9534c/example/src/main/res/drawable-hdpi-v9/ic_block.png -------------------------------------------------------------------------------- /example/src/main/res/drawable-hdpi/ic_block.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChatSecure/ChatSecure-Push-Android/2cf8f066bcf2ae062a84d63bdb324a89a3b9534c/example/src/main/res/drawable-hdpi/ic_block.png -------------------------------------------------------------------------------- /example/src/main/res/drawable-mdpi-v11/ic_block.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChatSecure/ChatSecure-Push-Android/2cf8f066bcf2ae062a84d63bdb324a89a3b9534c/example/src/main/res/drawable-mdpi-v11/ic_block.png -------------------------------------------------------------------------------- /example/src/main/res/drawable-mdpi-v9/ic_block.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChatSecure/ChatSecure-Push-Android/2cf8f066bcf2ae062a84d63bdb324a89a3b9534c/example/src/main/res/drawable-mdpi-v9/ic_block.png -------------------------------------------------------------------------------- /example/src/main/res/drawable-mdpi/ic_block.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChatSecure/ChatSecure-Push-Android/2cf8f066bcf2ae062a84d63bdb324a89a3b9534c/example/src/main/res/drawable-mdpi/ic_block.png -------------------------------------------------------------------------------- /example/src/main/res/drawable-xhdpi-v11/ic_block.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChatSecure/ChatSecure-Push-Android/2cf8f066bcf2ae062a84d63bdb324a89a3b9534c/example/src/main/res/drawable-xhdpi-v11/ic_block.png -------------------------------------------------------------------------------- /example/src/main/res/drawable-xhdpi-v9/ic_block.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChatSecure/ChatSecure-Push-Android/2cf8f066bcf2ae062a84d63bdb324a89a3b9534c/example/src/main/res/drawable-xhdpi-v9/ic_block.png -------------------------------------------------------------------------------- /example/src/main/res/drawable-xhdpi/ic_block.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChatSecure/ChatSecure-Push-Android/2cf8f066bcf2ae062a84d63bdb324a89a3b9534c/example/src/main/res/drawable-xhdpi/ic_block.png -------------------------------------------------------------------------------- /example/src/main/res/drawable-xxhdpi-v11/ic_block.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChatSecure/ChatSecure-Push-Android/2cf8f066bcf2ae062a84d63bdb324a89a3b9534c/example/src/main/res/drawable-xxhdpi-v11/ic_block.png -------------------------------------------------------------------------------- /example/src/main/res/drawable-xxhdpi-v9/ic_block.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChatSecure/ChatSecure-Push-Android/2cf8f066bcf2ae062a84d63bdb324a89a3b9534c/example/src/main/res/drawable-xxhdpi-v9/ic_block.png -------------------------------------------------------------------------------- /example/src/main/res/drawable-xxhdpi/ic_block.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChatSecure/ChatSecure-Push-Android/2cf8f066bcf2ae062a84d63bdb324a89a3b9534c/example/src/main/res/drawable-xxhdpi/ic_block.png -------------------------------------------------------------------------------- /example/src/main/res/drawable-xxhdpi/ic_menu_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChatSecure/ChatSecure-Push-Android/2cf8f066bcf2ae062a84d63bdb324a89a3b9534c/example/src/main/res/drawable-xxhdpi/ic_menu_black_24dp.png -------------------------------------------------------------------------------- /example/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 13 | 14 | 20 | 21 | 27 | 28 | 29 | 30 | 31 | 38 | -------------------------------------------------------------------------------- /example/src/main/res/layout/device_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 13 | 14 | 19 | 20 |