Skip to content

Commit 12819ab

Browse files
committed
feat(app/common/server): add update checking
Signed-off-by: Trumeet <[email protected]>
1 parent d877379 commit 12819ab

File tree

15 files changed

+675
-25
lines changed

15 files changed

+675
-25
lines changed

app/src/main/java/moe/yuuta/mipushtester/MainFragment.java

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,10 @@
2323
import android.view.ViewGroup;
2424
import android.widget.Toast;
2525

26+
import com.elvishew.xlog.Logger;
2627
import com.elvishew.xlog.XLog;
2728
import com.google.android.gms.oss.licenses.OssLicensesMenuActivity;
29+
import com.google.android.material.snackbar.Snackbar;
2830
import com.xiaomi.mipush.sdk.MiPushClient;
2931

3032
import androidx.annotation.NonNull;
@@ -36,13 +38,22 @@
3638
import androidx.navigation.Navigation;
3739
import moe.yuuta.mipushtester.databinding.FragmentMainBinding;
3840
import moe.yuuta.mipushtester.log.LogUtils;
41+
import moe.yuuta.mipushtester.push.APIManager;
3942
import moe.yuuta.mipushtester.status.RegistrationStatus;
43+
import moe.yuuta.mipushtester.update.Update;
44+
import retrofit2.Call;
45+
import retrofit2.Callback;
46+
import retrofit2.Response;
4047

4148
import static android.content.Context.MODE_PRIVATE;
4249
import static android.os.Looper.getMainLooper;
4350

4451
public class MainFragment extends Fragment implements MainFragmentUIHandler {
52+
private Logger logger = XLog.tag(MainFragment.class.getSimpleName()).build();
53+
4554
private RegistrationStatus mRegistrationStatus;
55+
private FragmentMainBinding binding;
56+
private Call<Update> mGetUpdateCall;
4657

4758
@Override
4859
public void onCreate(@Nullable Bundle savedInstanceState) {
@@ -53,13 +64,50 @@ public void onCreate(@Nullable Bundle savedInstanceState) {
5364
@Nullable
5465
@Override
5566
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
56-
FragmentMainBinding binding = DataBindingUtil.inflate(inflater, R.layout.fragment_main, container, false);
67+
binding = DataBindingUtil.inflate(inflater, R.layout.fragment_main, container, false);
5768
mRegistrationStatus = RegistrationStatus.get(requireContext());
5869
binding.setStatus(mRegistrationStatus);
5970
binding.setUiHandler(this);
6071
return binding.getRoot();
6172
}
6273

74+
@Override
75+
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
76+
super.onViewCreated(view, savedInstanceState);
77+
mGetUpdateCall = APIManager.getInstance().getUpdate();
78+
mGetUpdateCall.enqueue(new Callback<Update>() {
79+
@Override
80+
public void onResponse(@NonNull Call<Update> call, @NonNull Response<Update> response) {
81+
if (call.isCanceled()) return;
82+
if (!response.isSuccessful()) return;
83+
Update result = response.body();
84+
if (result == null) return;
85+
if (result.getVersionCode() < BuildConfig.VERSION_CODE) return;
86+
Snackbar.make(binding.getRoot(), getString(R.string.update_available,
87+
result.getVersionName()), Snackbar.LENGTH_SHORT)
88+
.setAction(R.string.view, v -> {
89+
String url =
90+
shouldOpenGooglePlay() ?
91+
"https://play.google.com/store/apps/details?id=" + BuildConfig.APPLICATION_ID
92+
: result.getHtmlLink();
93+
try {
94+
startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(url)));
95+
} catch (ActivityNotFoundException ignored) {}
96+
}).show();
97+
}
98+
99+
private boolean shouldOpenGooglePlay () {
100+
return "com.android.vending".equals(requireContext().getPackageManager().getInstallerPackageName(BuildConfig.APPLICATION_ID));
101+
}
102+
103+
@Override
104+
public void onFailure(@NonNull Call<Update> call, @NonNull Throwable t) {
105+
if (call.isCanceled()) return;
106+
logger.e("Unable to get update", t);
107+
}
108+
});
109+
}
110+
63111
public void handleToggleRegister (View v) {
64112
mRegistrationStatus.fetchStatus(requireContext());
65113
if (!mRegistrationStatus.registered.get()) {
@@ -147,4 +195,10 @@ public boolean onOptionsItemSelected(@NonNull MenuItem item) {
147195
public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) {
148196
inflater.inflate(R.menu.menu_main, menu);
149197
}
198+
199+
@Override
200+
public void onDestroyView() {
201+
if (mGetUpdateCall != null) mGetUpdateCall.cancel();
202+
super.onDestroyView();
203+
}
150204
}

app/src/main/java/moe/yuuta/mipushtester/push/APIInterface.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,16 @@
22

33
import com.google.gson.JsonObject;
44

5+
import moe.yuuta.mipushtester.update.Update;
56
import retrofit2.Call;
67
import retrofit2.http.Body;
8+
import retrofit2.http.GET;
79
import retrofit2.http.POST;
810

911
public interface APIInterface {
1012
@POST("/test")
1113
Call<JsonObject> push (@Body PushRequest request);
14+
15+
@GET("/update")
16+
Call<Update> getUpdate ();
1217
}
Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,39 @@
11
package moe.yuuta.mipushtester.push;
22

3+
import com.elvishew.xlog.Logger;
4+
import com.elvishew.xlog.XLog;
35
import com.google.gson.JsonObject;
46

5-
import java.io.IOException;
67
import java.util.Locale;
78

89
import androidx.annotation.NonNull;
910
import moe.yuuta.common.Constants;
1011
import moe.yuuta.mipushtester.BuildConfig;
12+
import moe.yuuta.mipushtester.update.Update;
1113
import okhttp3.OkHttpClient;
1214
import okhttp3.Request;
1315
import retrofit2.Call;
1416
import retrofit2.Retrofit;
1517
import retrofit2.converter.gson.GsonConverterFactory;
1618

1719
public class APIManager {
20+
private final Logger logger = XLog.tag(APIManager.class.getSimpleName()).build();
21+
1822
private APIInterface apiInterface;
1923
private static APIManager instance;
2024
public static @NonNull APIManager getInstance() {
2125
if (instance == null) {
2226
instance = new APIManager();
2327
OkHttpClient.Builder builder = new OkHttpClient.Builder()
2428
.addInterceptor(chain -> {
25-
try {
26-
Request original = chain.request();
27-
28-
Request.Builder originalBuilder = original.newBuilder();
29-
originalBuilder.addHeader(Constants.HEADER_LOCALE, Locale.getDefault().toString())
30-
.addHeader(Constants.HEADER_VERSION, BuildConfig.VERSION_NAME);
31-
Request request = originalBuilder.build();
32-
return chain.proceed(request);
33-
} catch (IOException ignored) {
34-
return null;
35-
}
29+
Request original = chain.request();
30+
31+
Request.Builder originalBuilder = original.newBuilder();
32+
originalBuilder.addHeader(Constants.HEADER_LOCALE, Locale.getDefault().toString())
33+
.addHeader(Constants.HEADER_VERSION, BuildConfig.VERSION_NAME)
34+
.addHeader(Constants.HEADER_PRODUCT, BuildConfig.APPLICATION_ID);
35+
Request request = originalBuilder.build();
36+
return chain.proceed(request);
3637
});
3738
instance.apiInterface = new Retrofit.Builder()
3839
.addConverterFactory(GsonConverterFactory.create())
@@ -46,4 +47,8 @@ public class APIManager {
4647
public Call<JsonObject> push (@NonNull PushRequest request) {
4748
return apiInterface.push(request);
4849
}
50+
51+
public Call<Update> getUpdate () {
52+
return apiInterface.getUpdate();
53+
}
4954
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package moe.yuuta.mipushtester.update;
2+
3+
import com.google.gson.annotations.SerializedName;
4+
5+
import java.util.Objects;
6+
7+
public class Update {
8+
@SerializedName("version_name")
9+
private String versionName;
10+
@SerializedName("version_code")
11+
private int versionCode;
12+
@SerializedName("html_link")
13+
private String htmlLink;
14+
15+
public String getVersionName() {
16+
return versionName;
17+
}
18+
19+
public void setVersionName(String versionName) {
20+
this.versionName = versionName;
21+
}
22+
23+
public int getVersionCode() {
24+
return versionCode;
25+
}
26+
27+
public void setVersionCode(int versionCode) {
28+
this.versionCode = versionCode;
29+
}
30+
31+
public String getHtmlLink() {
32+
return htmlLink;
33+
}
34+
35+
public void setHtmlLink(String htmlLink) {
36+
this.htmlLink = htmlLink;
37+
}
38+
39+
@Override
40+
public boolean equals(Object o) {
41+
if (this == o) return true;
42+
if (o == null || getClass() != o.getClass()) return false;
43+
Update update = (Update) o;
44+
return versionCode == update.versionCode &&
45+
Objects.equals(versionName, update.versionName) &&
46+
Objects.equals(htmlLink, update.htmlLink);
47+
}
48+
49+
@Override
50+
public int hashCode() {
51+
return Objects.hash(versionName, versionCode, htmlLink);
52+
}
53+
}

app/src/main/res/values/strings.xml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,4 +91,6 @@
9191
<string name="share_logs_chooser_title">Send logs to…</string>
9292
<string name="view_on_github">View on GitHub</string>
9393
<string name="error_send_push_need_register">Sending push requires registration at first</string>
94+
<string name="update_available">Update available: %1$s</string>
95+
<string name="view">View</string>
9496
</resources>

app/xmpush.properties.template

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
appId=123
33
appKey=123
44
# The PackageName of client
5-
# You should change common/src/main/java/moe/yuuta/common/Constants.java#CLIENT_ID
5+
# You should change common/src/main/java/moe/yuuta/common/Constants.java#TESTER_CLIENT_ID
66
app.id=com.example.mipushtester
77
# Signing configuration
88
key.locate=/your/sign.jks

common/src/main/java/moe/yuuta/common/Constants.java

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,14 @@ public class Constants {
77
public static final int DISPLAY_SOUND = 1;
88
public static final int DISPLAY_VIBRATE = 2;
99
public static final int DISPLAY_LIGHTS = 3;
10-
public static final String HEADER_LOCALE = "X-MiPushTester-Local";
11-
public static final String HEADER_VERSION = "X-MiPushTester-Version";
10+
public static final String HEADER_LOCALE = "X-MiPush-Local";
11+
public static final String HEADER_VERSION = "X-MiPush-Version";
12+
public static final String HEADER_PRODUCT = "X-MiPush-Product";
1213
public static final String EXTRA_MIPUSHTESTER_PREFIX = "mpt-";
1314
public static final String EXTRA_REQUEST_LOCALE = EXTRA_MIPUSHTESTER_PREFIX + "request_locale";
1415
public static final String EXTRA_REQUEST_TIME = EXTRA_MIPUSHTESTER_PREFIX + "request_time";
1516
public static final String EXTRA_CLIENT_VERSION = EXTRA_MIPUSHTESTER_PREFIX + "client_version";
1617
// The package name of client, for more details, see BUILD.md
17-
public static final String CLIENT_ID = "moe.yuuta.mipushtester";
18+
public static final String TESTER_CLIENT_ID = "moe.yuuta.mipushtester";
19+
public static final String FRAMEWORK_CLIENT_ID = "top.trumeet.mipush";
1820
}

server/src/main/java/moe/yuuta/server/api/ApiHandler.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import io.vertx.core.Vertx;
44
import io.vertx.ext.web.RoutingContext;
5+
import moe.yuuta.server.github.GitHubApi;
56
import moe.yuuta.server.mipush.MiPushApi;
67

78
public interface ApiHandler {
@@ -13,4 +14,6 @@ static ApiHandler apiHandler(Vertx vertx) {
1314
void handleFrameworkIndex(RoutingContext routingContext);
1415
void handleTesterIndex (RoutingContext routingContext);
1516
MiPushApi getMiPushApi ();
17+
void handleUpdate(RoutingContext routingContext);
18+
GitHubApi getGitHubApi ();
1619
}

server/src/main/java/moe/yuuta/server/api/ApiHandlerImpl.java

Lines changed: 69 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,21 @@
88
import java.util.Map;
99
import java.util.TimeZone;
1010

11-
import io.netty.handler.codec.http.HttpResponseStatus;
1211
import io.vertx.core.Vertx;
1312
import io.vertx.core.logging.Logger;
1413
import io.vertx.core.logging.LoggerFactory;
1514
import io.vertx.ext.web.RoutingContext;
1615
import moe.yuuta.common.Constants;
16+
import moe.yuuta.server.api.update.Update;
1717
import moe.yuuta.server.dataverify.DataVerifier;
18+
import moe.yuuta.server.github.GitHubApi;
19+
import moe.yuuta.server.github.Release;
1820
import moe.yuuta.server.mipush.Message;
1921
import moe.yuuta.server.mipush.MiPushApi;
2022
import moe.yuuta.server.mipush.SendMessageResponse;
2123
import moe.yuuta.server.res.Resources;
2224

25+
import static io.netty.handler.codec.http.HttpResponseStatus.NO_CONTENT;
2326
import static moe.yuuta.common.Constants.DISPLAY_ALL;
2427
import static moe.yuuta.common.Constants.DISPLAY_LIGHTS;
2528
import static moe.yuuta.common.Constants.DISPLAY_SOUND;
@@ -86,7 +89,7 @@ public void handlePush(RoutingContext routingContext) {
8689
new SimpleDateFormat("HH:mm:ss", Locale.CHINA).format(Calendar.getInstance(TimeZone.getTimeZone("Asia/Shanghai")).getTime()));
8790
Message message = new Message();
8891
message.setTicker(ticker);
89-
message.setRestrictedPackageName(Constants.CLIENT_ID);
92+
message.setRestrictedPackageName(Constants.TESTER_CLIENT_ID);
9093
// FIXME
9194
message.setPassThrough(request.isPassThrough() ? Message.PASS_THROUGH_ENABLED :
9295
Message.PASS_THROUGH_DISABLED);
@@ -148,7 +151,7 @@ public void handlePush(RoutingContext routingContext) {
148151
SendMessageResponse response = ar.result().body();
149152
routingContext.response()
150153
.setStatusCode(response.getCode() == SendMessageResponse.CODE_SUCCESS ?
151-
HttpResponseStatus.NO_CONTENT.code() : 500)
154+
NO_CONTENT.code() : 500)
152155
.end();
153156
} else {
154157
logger.error("Cannot send message", ar.cause());
@@ -172,4 +175,67 @@ public void handleTesterIndex(RoutingContext routingContext) {
172175
.setStatusCode(200)
173176
.end(HTML_TESTER_INDEX);
174177
}
178+
179+
@Override
180+
public void handleUpdate(RoutingContext routingContext) {
181+
final String productId = routingContext.request().getHeader(Constants.HEADER_PRODUCT);
182+
if (productId == null) {
183+
routingContext.response().setStatusCode(NO_CONTENT.code()).end();
184+
return;
185+
}
186+
String repo;
187+
String owner;
188+
switch (productId) {
189+
// Only "Authorized" offical clients can access this service.
190+
case Constants.TESTER_CLIENT_ID:
191+
repo = "MiPushTester";
192+
owner = "Trumeet";
193+
break;
194+
case Constants.FRAMEWORK_CLIENT_ID:
195+
repo = "MiPushFramework";
196+
owner = "Trumeet";
197+
break;
198+
default:
199+
logger.warn("An unknown client is attempting to get update status: " + productId);
200+
routingContext.response().setStatusCode(NO_CONTENT.code()).end();
201+
return;
202+
}
203+
getGitHubApi().getLatestRelease(owner, repo, ar -> {
204+
if (ar.succeeded()) {
205+
Release release = ar.result().body();
206+
if (release == null
207+
|| release.getName() == null
208+
|| release.getTagName() == null
209+
|| release.getName().trim().equals("")
210+
|| release.getTagName().trim().equals("")) {
211+
routingContext.response().setStatusCode(NO_CONTENT.code()).end();
212+
} else {
213+
Update update = new Update();
214+
update.setHtmlLink(release.getHtmlUrl());
215+
try {
216+
update.setVersionCode(Integer.parseInt(release.getTagName()));
217+
} catch (NumberFormatException ignored) {
218+
update.setVersionCode(Integer.MAX_VALUE);
219+
}
220+
update.setVersionName(release.getName());
221+
routingContext.response()
222+
.putHeader("Content-Type", "application/json")
223+
.setChunked(true)
224+
.setStatusCode(200)
225+
.end(ApiUtils.tryObjectToJson(update));
226+
}
227+
} else {
228+
logger.error("Unable to get update", ar.cause());
229+
routingContext.response()
230+
.setChunked(true)
231+
.setStatusCode(500)
232+
.end();
233+
}
234+
});
235+
}
236+
237+
@Override
238+
public GitHubApi getGitHubApi() {
239+
return new GitHubApi(vertx.createHttpClient());
240+
}
175241
}

0 commit comments

Comments
 (0)